<?php

declare(strict_types=1);

namespace Roave\BetterReflection\Reflection;

use InvalidArgumentException;
use OutOfBoundsException;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_ as ClassNode;
use PhpParser\Node\Stmt\ClassConst as ConstNode;
use PhpParser\Node\Stmt\ClassLike as ClassLikeNode;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Interface_ as InterfaceNode;
use PhpParser\Node\Stmt\Namespace_ as NamespaceNode;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Property as PropertyNode;
use PhpParser\Node\Stmt\Trait_ as TraitNode;
use PhpParser\Node\Stmt\TraitUse;
use ReflectionClass as CoreReflectionClass;
use ReflectionException;
use ReflectionProperty as CoreReflectionProperty;
use Reflector as CoreReflector;
use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflection\Exception\ClassDoesNotExist;
use Roave\BetterReflection\Reflection\Exception\NoObjectProvided;
use Roave\BetterReflection\Reflection\Exception\NotAClassReflection;
use Roave\BetterReflection\Reflection\Exception\NotAnInterfaceReflection;
use Roave\BetterReflection\Reflection\Exception\NotAnObject;
use Roave\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass;
use Roave\BetterReflection\Reflection\Exception\PropertyDoesNotExist;
use Roave\BetterReflection\Reflection\Exception\Uncloneable;
use Roave\BetterReflection\Reflection\StringCast\ReflectionClassStringCast;
use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound;
use Roave\BetterReflection\Reflector\Reflector;
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
use Roave\BetterReflection\Util\CalculateReflectionColum;
use Roave\BetterReflection\Util\GetFirstDocComment;
use Traversable;
use function array_combine;
use function array_filter;
use function array_map;
use function array_merge;
use function array_reverse;
use function array_slice;
use function array_values;
use function implode;
use function in_array;
use function is_object;
use function ltrim;
use function sha1;
use function sprintf;
use function strtolower;

class ReflectionClass implements Reflection, CoreReflector
{
    public const ANONYMOUS_CLASS_NAME_PREFIX = 'class@anonymous';

    /** @var Reflector */
    private $reflector;

    /** @var NamespaceNode|null */
    private $declaringNamespace;

    /** @var LocatedSource */
    private $locatedSource;

    /** @var ClassLikeNode */
    private $node;

    /** @var ReflectionClassConstant[]|null indexed by name, when present */
    private $cachedReflectionConstants;

    /** @var ReflectionProperty[]|null */
    private $cachedImmediateProperties;

    /** @var ReflectionProperty[]|null */
    private $cachedProperties;

    /** @var ReflectionMethod[]|null */
    private $cachedMethods;

    private function __construct()
    {
    }

    /**
     * Create a reflection and return the string representation of a named class
     */
    public static function export(?string $className) : string
    {
        if ($className === null) {
            throw new InvalidArgumentException('Class name must be provided');
        }

        return self::createFromName($className)->__toString();
    }

    public function __toString() : string
    {
        return ReflectionClassStringCast::toString($this);
    }

    /**
     * Create a ReflectionClass by name, using default reflectors etc.
     *
     * @throws IdentifierNotFound
     */
    public static function createFromName(string $className) : self
    {
        return (new BetterReflection())->classReflector()->reflect($className);
    }

    /**
     * Create a ReflectionClass from an instance, using default reflectors etc.
     *
     * This is simply a helper method that calls ReflectionObject::createFromInstance().
     *
     * @see ReflectionObject::createFromInstance
     *
     * @param object $instance
     *
     * @throws IdentifierNotFound
     * @throws ReflectionException
     * @throws InvalidArgumentException
     *
     * @psalm-suppress DocblockTypeContradiction
     */
    public static function createFromInstance($instance) : self
    {
        if (! is_object($instance)) {
            throw new InvalidArgumentException('Instance must be an instance of an object');
        }

        return ReflectionObject::createFromInstance($instance);
    }

    /**
     * Create from a Class Node.
     *
     * @internal
     *
     * @param ClassLikeNode      $node      Node has to be processed by the PhpParser\NodeVisitor\NameResolver
     * @param NamespaceNode|null $namespace optional - if omitted, we assume it is global namespaced class
     */
    public static function createFromNode(
        Reflector $reflector,
        ClassLikeNode $node,
        LocatedSource $locatedSource,
        ?NamespaceNode $namespace = null
    ) : self {
        $class = new self();

        $class->reflector     = $reflector;
        $class->locatedSource = $locatedSource;
        $class->node          = $node;

        if ($namespace !== null) {
            $class->declaringNamespace = $namespace;
        }

        return $class;
    }

    /**
     * Get the "short" name of the class (e.g. for A\B\Foo, this will return
     * "Foo").
     */
    public function getShortName() : string
    {
        if (! $this->isAnonymous()) {
            /** @var Node\Identifier $this->node->name */
            return $this->node->name->name;
        }

        $fileName = $this->getFileName();

        if ($fileName === null) {
            $fileName = sha1($this->locatedSource->getSource());
        }

        return sprintf('%s%c%s(%d)', self::ANONYMOUS_CLASS_NAME_PREFIX, "\0", $fileName, $this->getStartLine());
    }

    /**
     * Get the "full" name of the class (e.g. for A\B\Foo, this will return
     * "A\B\Foo").
     */
    public function getName() : string
    {
        if (! $this->inNamespace()) {
            return $this->getShortName();
        }

        return $this->node->namespacedName->toString();
    }

    /**
     * Get the "namespace" name of the class (e.g. for A\B\Foo, this will
     * return "A\B").
     *
     * @psalm-suppress PossiblyNullPropertyFetch
     */
    public function getNamespaceName() : string
    {
        if (! $this->inNamespace()) {
            return '';
        }

        return implode('\\', $this->declaringNamespace->name->parts);
    }

    /**
     * Decide if this class is part of a namespace. Returns false if the class
     * is in the global namespace or does not have a specified namespace.
     */
    public function inNamespace() : bool
    {
        return $this->declaringNamespace !== null
            && $this->declaringNamespace->name !== null;
    }

    public function getExtensionName() : ?string
    {
        return $this->locatedSource->getExtensionName();
    }

    /**
     * Construct a flat list of all methods from current class, traits,
     * parent classes and interfaces in this precise order.
     *
     * @return ReflectionMethod[]
     */
    private function getAllMethods() : array
    {
        return array_merge(
            [],
            array_map(
                function (ClassMethod $methodNode) : ReflectionMethod {
                    return ReflectionMethod::createFromNode(
                        $this->reflector,
                        $methodNode,
                        $this->declaringNamespace,
                        $this,
                        $this
                    );
                },
                $this->node->getMethods()
            ),
            ...array_map(
                function (ReflectionClass $trait) : array {
                    return array_map(function (ReflectionMethod $method) use ($trait) : ReflectionMethod {
                        /** @var ClassMethod $methodAst */
                        $methodAst = $method->getAst();

                        return ReflectionMethod::createFromNode(
                            $this->reflector,
                            $methodAst,
                            $this->declaringNamespace,
                            $trait,
                            $this
                        );
                    }, $trait->getMethods());
                },
                $this->getTraits()
            ),
            ...array_map(
                static function (ReflectionClass $ancestor) : array {
                    return $ancestor->getMethods();
                },
                array_values(array_merge(
                    array_filter([$this->getParentClass()]),
                    $this->getInterfaces()
                ))
            )
        );
    }

    /**
     * Construct a flat list of methods that are available. This will search up
     * all parent classes/traits/interfaces/current scope for methods.
     *
     * Methods are not merged via their name as array index, since internal PHP method
     * sorting does not follow `\array_merge()` semantics.
     *
     * @return ReflectionMethod[] indexed by method name
     */
    private function getMethodsIndexedByName() : array
    {
        if ($this->cachedMethods !== null) {
            return $this->cachedMethods;
        }

        $this->cachedMethods = [];

        $traitAliases = $this->getTraitAliases();

        foreach ($this->getAllMethods() as $method) {
            $methodName              = $method->getName();
            $methodNameWithClassName = sprintf('%s::%s', $method->getDeclaringClass()->getName(), $methodName);

            foreach ($traitAliases as $methodAlias => $traitMethodNameWithTraitName) {
                if ($methodNameWithClassName !== $traitMethodNameWithTraitName) {
                    continue;
                }

                if (isset($this->cachedMethods[$methodAlias])) {
                    continue;
                }

                $this->cachedMethods[$methodAlias] = $method;
            }

            if (isset($this->cachedMethods[$methodName])) {
                continue;
            }

            $this->cachedMethods[$methodName] = $method;
        }

        return $this->cachedMethods;
    }

    /**
     * Fetch an array of all methods for this class.
     *
     * Filter the results to include only methods with certain attributes. Defaults
     * to no filtering.
     * Any combination of \ReflectionMethod::IS_STATIC,
     * \ReflectionMethod::IS_PUBLIC,
     * \ReflectionMethod::IS_PROTECTED,
     * \ReflectionMethod::IS_PRIVATE,
     * \ReflectionMethod::IS_ABSTRACT,
     * \ReflectionMethod::IS_FINAL.
     * For example if $filter = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_FINAL
     * the only the final public methods will be returned
     *
     * @return ReflectionMethod[]
     */
    public function getMethods(?int $filter = null) : array
    {
        if ($filter === null) {
            return array_values($this->getMethodsIndexedByName());
        }

        return array_values(
            array_filter(
                $this->getMethodsIndexedByName(),
                static function (ReflectionMethod $method) use ($filter) : bool {
                    return (bool) ($filter & $method->getModifiers());
                }
            )
        );
    }

    /**
     * Get only the methods that this class implements (i.e. do not search
     * up parent classes etc.)
     *
     * @see ReflectionClass::getMethods for the usage of $filter
     *
     * @return ReflectionMethod[]
     */
    public function getImmediateMethods(?int $filter = null) : array
    {
        /** @var ReflectionMethod[] $methods */
        $methods = array_map(
            function (ClassMethod $methodNode) : ReflectionMethod {
                return ReflectionMethod::createFromNode(
                    $this->reflector,
                    $methodNode,
                    $this->declaringNamespace,
                    $this,
                    $this
                );
            },
            $this->node->getMethods()
        );

        $methodsByName = [];

        foreach ($methods as $method) {
            if ($filter !== null && ! ($filter & $method->getModifiers())) {
                continue;
            }

            $methodsByName[$method->getName()] = $method;
        }

        return $methodsByName;
    }

    /**
     * Get a single method with the name $methodName.
     *
     * @throws OutOfBoundsException
     */
    public function getMethod(string $methodName) : ReflectionMethod
    {
        $methods = $this->getMethodsIndexedByName();

        if (! isset($methods[$methodName])) {
            throw new OutOfBoundsException('Could not find method: ' . $methodName);
        }

        return $methods[$methodName];
    }

    /**
     * Does the class have the specified method method?
     */
    public function hasMethod(string $methodName) : bool
    {
        try {
            $this->getMethod($methodName);

            return true;
        } catch (OutOfBoundsException $exception) {
            return false;
        }
    }

    /**
     * Get an associative array of only the constants for this specific class (i.e. do not search
     * up parent classes etc.), with keys as constant names and values as constant values.
     *
     * @return string[]|int[]|float[]|array[]|bool[]|null[]
     */
    public function getImmediateConstants() : array
    {
        return array_map(static function (ReflectionClassConstant $classConstant) {
            return $classConstant->getValue();
        }, $this->getImmediateReflectionConstants());
    }

    /**
     * Get an associative array of the defined constants in this class,
     * with keys as constant names and values as constant values.
     *
     * @return string[]|int[]|float[]|array[]|bool[]|null[]
     */
    public function getConstants() : array
    {
        return array_map(static function (ReflectionClassConstant $classConstant) {
            return $classConstant->getValue();
        }, $this->getReflectionConstants());
    }

    /**
     * Get the value of the specified class constant.
     *
     * Returns null if not specified.
     *
     * @return mixed|null
     */
    public function getConstant(string $name)
    {
        $reflectionConstant = $this->getReflectionConstant($name);

        if (! $reflectionConstant) {
            return null;
        }

        return $reflectionConstant->getValue();
    }

    /**
     * Does this class have the specified constant?
     */
    public function hasConstant(string $name) : bool
    {
        return $this->getReflectionConstant($name) !== null;
    }

    /**
     * Get the reflection object of the specified class constant.
     *
     * Returns null if not specified.
     */
    public function getReflectionConstant(string $name) : ?ReflectionClassConstant
    {
        return $this->getReflectionConstants()[$name] ?? null;
    }

    /**
     * Get an associative array of only the constants for this specific class (i.e. do not search
     * up parent classes etc.), with keys as constant names and values as {@see ReflectionClassConstant} objects.
     *
     * @return ReflectionClassConstant[] indexed by name
     */
    public function getImmediateReflectionConstants() : array
    {
        if ($this->cachedReflectionConstants !== null) {
            return $this->cachedReflectionConstants;
        }

        $constants = array_merge(
            [],
            ...array_map(
                function (ConstNode $constNode) : array {
                    $constants = [];

                    foreach ($constNode->consts as $constantPositionInNode => $constantNode) {
                        $constants[] = ReflectionClassConstant::createFromNode($this->reflector, $constNode, $constantPositionInNode, $this);
                    }

                    return $constants;
                },
                array_filter(
                    $this->node->stmts,
                    static function (Node\Stmt $stmt) : bool {
                        return $stmt instanceof ConstNode;
                    }
                )
            )
        );

        return $this->cachedReflectionConstants = array_combine(
            array_map(
                static function (ReflectionClassConstant $constant) : string {
                    return $constant->getName();
                },
                $constants
            ),
            $constants
        );
    }

    /**
     * Get an associative array of the defined constants in this class,
     * with keys as constant names and values as {@see ReflectionClassConstant} objects.
     *
     * @return ReflectionClassConstant[] indexed by name
     */
    public function getReflectionConstants() : array
    {
        // Note: constants are not merged via their name as array index, since internal PHP constant
        //       sorting does not follow `\array_merge()` semantics
        /** @var ReflectionClassConstant[] $allReflectionConstants */
        $allReflectionConstants = array_merge(
            array_values($this->getImmediateReflectionConstants()),
            ...array_map(
                static function (ReflectionClass $ancestor) : array {
                    return array_filter(
                        array_values($ancestor->getReflectionConstants()),
                        static function (ReflectionClassConstant $classConstant) : bool {
                            return ! $classConstant->isPrivate();
                        }
                    );
                },
                array_filter([$this->getParentClass()])
            ),
            ...array_map(
                static function (ReflectionClass $interface) : array {
                    return array_values($interface->getReflectionConstants());
                },
                array_values($this->getInterfaces())
            )
        );

        $reflectionConstants = [];

        foreach ($allReflectionConstants as $constant) {
            $constantName = $constant->getName();

            if (isset($reflectionConstants[$constantName])) {
                continue;
            }

            $reflectionConstants[$constantName] = $constant;
        }

        return $reflectionConstants;
    }

    /**
     * Get the constructor method for this class.
     *
     * @throws OutOfBoundsException
     */
    public function getConstructor() : ReflectionMethod
    {
        $constructors = array_filter($this->getMethods(), static function (ReflectionMethod $method) : bool {
            return $method->isConstructor();
        });

        if (! isset($constructors[0])) {
            throw new OutOfBoundsException('Could not find method: __construct');
        }

        return $constructors[0];
    }

    /**
     * Get only the properties for this specific class (i.e. do not search
     * up parent classes etc.)
     *
     * @see ReflectionClass::getProperties() for the usage of filter
     *
     * @return ReflectionProperty[]
     */
    public function getImmediateProperties(?int $filter = null) : array
    {
        if ($this->cachedImmediateProperties === null) {
            $properties = [];
            foreach ($this->node->stmts as $stmt) {
                if (! ($stmt instanceof PropertyNode)) {
                    continue;
                }

                foreach ($stmt->props as $propertyPositionInNode => $propertyNode) {
                    $prop                         = ReflectionProperty::createFromNode(
                        $this->reflector,
                        $stmt,
                        $propertyPositionInNode,
                        $this->declaringNamespace,
                        $this,
                        $this
                    );
                    $properties[$prop->getName()] = $prop;
                }
            }

            $this->cachedImmediateProperties = $properties;
        }

        if ($filter === null) {
            return $this->cachedImmediateProperties;
        }

        return array_filter(
            $this->cachedImmediateProperties,
            static function (ReflectionProperty $property) use ($filter) : bool {
                return (bool) ($filter & $property->getModifiers());
            }
        );
    }

    /**
     * Get the properties for this class.
     *
     * Filter the results to include only properties with certain attributes. Defaults
     * to no filtering.
     * Any combination of \ReflectionProperty::IS_STATIC,
     * \ReflectionProperty::IS_PUBLIC,
     * \ReflectionProperty::IS_PROTECTED,
     * \ReflectionProperty::IS_PRIVATE.
     * For example if $filter = \ReflectionProperty::IS_STATIC | \ReflectionProperty::IS_PUBLIC
     * only the static public properties will be returned
     *
     * @return ReflectionProperty[]
     */
    public function getProperties(?int $filter = null) : array
    {
        if ($this->cachedProperties === null) {
            // merging together properties from parent class, traits, current class (in this precise order)
            $this->cachedProperties = array_merge(
                array_merge(
                    [],
                    ...array_map(
                        static function (ReflectionClass $ancestor) use ($filter) : array {
                            return array_filter(
                                $ancestor->getProperties($filter),
                                static function (ReflectionProperty $property) : bool {
                                    return ! $property->isPrivate();
                                }
                            );
                        },
                        array_filter([$this->getParentClass()])
                    ),
                    ...array_map(
                        function (ReflectionClass $trait) use ($filter) {
                            return array_map(function (ReflectionProperty $property) use ($trait) : ReflectionProperty {
                                return ReflectionProperty::createFromNode(
                                    $this->reflector,
                                    $property->getAst(),
                                    $property->getPositionInAst(),
                                    $trait->declaringNamespace,
                                    $property->getDeclaringClass(),
                                    $this
                                );
                            }, $trait->getProperties($filter));
                        },
                        $this->getTraits()
                    )
                ),
                $this->getImmediateProperties()
            );
        }

        if ($filter === null) {
            return $this->cachedProperties;
        }

        return array_filter(
            $this->cachedProperties,
            static function (ReflectionProperty $property) use ($filter) : bool {
                return (bool) ($filter & $property->getModifiers());
            }
        );
    }

    /**
     * Get the property called $name.
     *
     * Returns null if property does not exist.
     */
    public function getProperty(string $name) : ?ReflectionProperty
    {
        $properties = $this->getProperties();

        if (! isset($properties[$name])) {
            return null;
        }

        return $properties[$name];
    }

    /**
     * Does this class have the specified property?
     */
    public function hasProperty(string $name) : bool
    {
        return $this->getProperty($name) !== null;
    }

    /**
     * @return mixed[]
     */
    public function getDefaultProperties() : array
    {
        return array_map(
            static function (ReflectionProperty $property) {
                return $property->getDefaultValue();
            },
            array_filter($this->getProperties(), static function (ReflectionProperty $property) : bool {
                return $property->isDefault();
            })
        );
    }

    public function getFileName() : ?string
    {
        return $this->locatedSource->getFileName();
    }

    public function getLocatedSource() : LocatedSource
    {
        return $this->locatedSource;
    }

    /**
     * Get the line number that this class starts on.
     */
    public function getStartLine() : int
    {
        return $this->node->getStartLine();
    }

    /**
     * Get the line number that this class ends on.
     */
    public function getEndLine() : int
    {
        return $this->node->getEndLine();
    }

    public function getStartColumn() : int
    {
        return CalculateReflectionColum::getStartColumn($this->locatedSource->getSource(), $this->node);
    }

    public function getEndColumn() : int
    {
        return CalculateReflectionColum::getEndColumn($this->locatedSource->getSource(), $this->node);
    }

    /**
     * Get the parent class, if it is defined. If this class does not have a
     * specified parent class, this will throw an exception.
     *
     * @throws NotAClassReflection
     */
    public function getParentClass() : ?ReflectionClass
    {
        if (! ($this->node instanceof ClassNode) || $this->node->extends === null) {
            return null;
        }

        // @TODO use actual `ClassReflector` or `FunctionReflector`?
        /** @var self $parent */
        $parent = $this->reflector->reflect($this->node->extends->toString());

        if ($parent->isInterface() || $parent->isTrait()) {
            throw NotAClassReflection::fromReflectionClass($parent);
        }

        return $parent;
    }

    /**
     * Gets the parent class names.
     *
     * @return string[] A numerical array with parent class names as the values.
     */
    public function getParentClassNames() : array
    {
        return array_map(static function (self $parentClass) : string {
            return $parentClass->getName();
        }, array_slice(array_reverse($this->getInheritanceClassHierarchy()), 1));
    }

    public function getDocComment() : string
    {
        return GetFirstDocComment::forNode($this->node);
    }

    public function isAnonymous() : bool
    {
        return $this->node->name === null;
    }

    /**
     * Is this an internal class?
     */
    public function isInternal() : bool
    {
        return $this->locatedSource->isInternal();
    }

    /**
     * Is this a user-defined function (will always return the opposite of
     * whatever isInternal returns).
     */
    public function isUserDefined() : bool
    {
        return ! $this->isInternal();
    }

    /**
     * Is this class an abstract class.
     */
    public function isAbstract() : bool
    {
        return $this->node instanceof ClassNode && $this->node->isAbstract();
    }

    /**
     * Is this class a final class.
     */
    public function isFinal() : bool
    {
        return $this->node instanceof ClassNode && $this->node->isFinal();
    }

    /**
     * Get the core-reflection-compatible modifier values.
     */
    public function getModifiers() : int
    {
        $val  = 0;
        $val += $this->isAbstract() ? CoreReflectionClass::IS_EXPLICIT_ABSTRACT : 0;
        $val += $this->isFinal() ? CoreReflectionClass::IS_FINAL : 0;

        return $val;
    }

    /**
     * Is this reflection a trait?
     */
    public function isTrait() : bool
    {
        return $this->node instanceof TraitNode;
    }

    /**
     * Is this reflection an interface?
     */
    public function isInterface() : bool
    {
        return $this->node instanceof InterfaceNode;
    }

    /**
     * Get the traits used, if any are defined. If this class does not have any
     * defined traits, this will return an empty array.
     *
     * @return ReflectionClass[]
     */
    public function getTraits() : array
    {
        return array_map(
            function (Node\Name $importedTrait) : ReflectionClass {
                return $this->reflectClassForNamedNode($importedTrait);
            },
            array_merge(
                [],
                ...array_map(
                    static function (TraitUse $traitUse) : array {
                        return $traitUse->traits;
                    },
                    array_filter($this->node->stmts, static function (Node $node) : bool {
                        return $node instanceof TraitUse;
                    })
                )
            )
        );
    }

    /**
     * Given an AST Node\Name, create a new ReflectionClass for the element.
     */
    private function reflectClassForNamedNode(Node\Name $node) : self
    {
        // @TODO use actual `ClassReflector` or `FunctionReflector`?
        if ($this->isAnonymous()) {
            /** @var self $class */
            $class = (new BetterReflection())->classReflector()->reflect($node->toString());
        } else {
            /** @var self $class */
            $class = $this->reflector->reflect($node->toString());
        }

        return $class;
    }

    /**
     * Get the names of the traits used as an array of strings, if any are
     * defined. If this class does not have any defined traits, this will
     * return an empty array.
     *
     * @return string[]
     */
    public function getTraitNames() : array
    {
        return array_map(
            static function (ReflectionClass $trait) : string {
                return $trait->getName();
            },
            $this->getTraits()
        );
    }

    /**
     * Return a list of the aliases used when importing traits for this class.
     * The returned array is in key/value pair in this format:.
     *
     *   'aliasedMethodName' => 'ActualClass::actualMethod'
     *
     * @return string[]
     *
     * @example
     * // When reflecting a class such as:
     * class Foo
     * {
     *     use MyTrait {
     *         myTraitMethod as myAliasedMethod;
     *     }
     * }
     * // This method would return
     * //   ['myAliasedMethod' => 'MyTrait::myTraitMethod']
     */
    public function getTraitAliases() : array
    {
        /** @var Node\Stmt\TraitUse[] $traitUsages */
        $traitUsages = array_filter($this->node->stmts, static function (Node $node) : bool {
            return $node instanceof TraitUse;
        });

        $resolvedAliases = [];

        foreach ($traitUsages as $traitUsage) {
            $traitNames  = $traitUsage->traits;
            $adaptations = $traitUsage->adaptations;

            foreach ($adaptations as $adaptation) {
                /** @var Node\Name|null $usedTrait */
                $usedTrait = $adaptation->trait;
                if ($usedTrait === null) {
                    $usedTrait = $traitNames[0];
                }

                if (! ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias)) {
                    continue;
                }

                if (! $adaptation->newName) {
                    continue;
                }

                $resolvedAliases[$adaptation->newName->name] = sprintf(
                    '%s::%s',
                    $usedTrait->toString(),
                    $adaptation->method->toString()
                );
            }
        }

        return $resolvedAliases;
    }

    /**
     * Gets the interfaces.
     *
     * @link https://php.net/manual/en/reflectionclass.getinterfaces.php
     *
     * @return ReflectionClass[] An associative array of interfaces, with keys as interface names and the array
     *                           values as {@see ReflectionClass} objects.
     */
    public function getInterfaces() : array
    {
        return array_merge(...array_map(
            static function (self $reflectionClass) : array {
                return $reflectionClass->getCurrentClassImplementedInterfacesIndexedByName();
            },
            $this->getInheritanceClassHierarchy()
        ));
    }

    /**
     * Get only the interfaces that this class implements (i.e. do not search
     * up parent classes etc.)
     *
     * @return ReflectionClass[]
     */
    public function getImmediateInterfaces() : array
    {
        return $this->getCurrentClassImplementedInterfacesIndexedByName();
    }

    /**
     * Gets the interface names.
     *
     * @link https://php.net/manual/en/reflectionclass.getinterfacenames.php
     *
     * @return string[] A numerical array with interface names as the values.
     */
    public function getInterfaceNames() : array
    {
        return array_values(array_map(
            static function (self $interface) : string {
                return $interface->getName();
            },
            $this->getInterfaces()
        ));
    }

    /**
     * Checks whether the given object is an instance.
     *
     * @link https://php.net/manual/en/reflectionclass.isinstance.php
     *
     * @param object $object
     *
     * @throws NotAnObject
     *
     * @psalm-suppress DocblockTypeContradiction
     */
    public function isInstance($object) : bool
    {
        if (! is_object($object)) {
            throw NotAnObject::fromNonObject($object);
        }

        $className = $this->getName();

        // note: since $object was loaded, we can safely assume that $className is available in the current
        //       php script execution context
        return $object instanceof $className;
    }

    /**
     * Checks whether the given class string is a subclass of this class.
     *
     * @link https://php.net/manual/en/reflectionclass.isinstance.php
     */
    public function isSubclassOf(string $className) : bool
    {
        return in_array(
            ltrim($className, '\\'),
            array_map(
                static function (self $reflectionClass) : string {
                    return $reflectionClass->getName();
                },
                array_slice(array_reverse($this->getInheritanceClassHierarchy()), 1)
            ),
            true
        );
    }

    /**
     * Checks whether this class implements the given interface.
     *
     * @link https://php.net/manual/en/reflectionclass.implementsinterface.php
     */
    public function implementsInterface(string $interfaceName) : bool
    {
        return in_array(ltrim($interfaceName, '\\'), $this->getInterfaceNames(), true);
    }

    /**
     * Checks whether this reflection is an instantiable class
     *
     * @link https://php.net/manual/en/reflectionclass.isinstantiable.php
     */
    public function isInstantiable() : bool
    {
        // @TODO doesn't consider internal non-instantiable classes yet.

        if ($this->isAbstract()) {
            return false;
        }

        if ($this->isInterface()) {
            return false;
        }

        if ($this->isTrait()) {
            return false;
        }

        try {
            return $this->getConstructor()->isPublic();
        } catch (OutOfBoundsException $e) {
            return true;
        }
    }

    /**
     * Checks whether this is a reflection of a class that supports the clone operator
     *
     * @link https://php.net/manual/en/reflectionclass.iscloneable.php
     */
    public function isCloneable() : bool
    {
        if (! $this->isInstantiable()) {
            return false;
        }

        if (! $this->hasMethod('__clone')) {
            return true;
        }

        return $this->getMethod('__clone')->isPublic();
    }

    /**
     * Checks if iterateable
     *
     * @link https://php.net/manual/en/reflectionclass.isiterateable.php
     */
    public function isIterateable() : bool
    {
        return $this->isInstantiable() && $this->implementsInterface(Traversable::class);
    }

    /**
     * @return ReflectionClass[] indexed by interface name
     */
    private function getCurrentClassImplementedInterfacesIndexedByName() : array
    {
        $node = $this->node;

        if ($node instanceof ClassNode) {
            return array_merge(
                [],
                ...array_map(
                    function (Node\Name $interfaceName) : array {
                        return $this
                            ->reflectClassForNamedNode($interfaceName)
                            ->getInterfacesHierarchy();
                    },
                    $node->implements
                )
            );
        }

        // assumption: first key is the current interface
        return $this->isInterface()
            ? array_slice($this->getInterfacesHierarchy(), 1)
            : [];
    }

    /**
     * @return ReflectionClass[] ordered from inheritance root to leaf (this class)
     */
    private function getInheritanceClassHierarchy() : array
    {
        $parentClass = $this->getParentClass();

        return $parentClass
            ? array_merge($parentClass->getInheritanceClassHierarchy(), [$this])
            : [$this];
    }

    /**
     * This method allows us to retrieve all interfaces parent of the this interface. Do not use on class nodes!
     *
     * @return ReflectionClass[] parent interfaces of this interface
     *
     * @throws NotAnInterfaceReflection
     */
    private function getInterfacesHierarchy() : array
    {
        if (! $this->isInterface()) {
            throw NotAnInterfaceReflection::fromReflectionClass($this);
        }

        /** @var InterfaceNode $node */
        $node = $this->node;

        return array_merge(
            [$this->getName() => $this],
            ...array_map(
                function (Node\Name $interfaceName) : array {
                    return $this
                        ->reflectClassForNamedNode($interfaceName)
                        ->getInterfacesHierarchy();
                },
                $node->extends
            )
        );
    }

    /**
     * @throws Uncloneable
     */
    public function __clone()
    {
        throw Uncloneable::fromClass(static::class);
    }

    /**
     * Get the value of a static property, if it exists. Throws a
     * PropertyDoesNotExist exception if it does not exist or is not static.
     * (note, differs very slightly from internal reflection behaviour)
     *
     * @return mixed
     *
     * @throws ClassDoesNotExist
     * @throws NoObjectProvided
     * @throws NotAnObject
     * @throws ObjectNotInstanceOfClass
     */
    public function getStaticPropertyValue(string $propertyName)
    {
        $property = $this->getProperty($propertyName);

        if (! $property || ! $property->isStatic()) {
            throw PropertyDoesNotExist::fromName($propertyName);
        }

        return $property->getValue();
    }

    /**
     * Set the value of a static property
     *
     * @param mixed $value
     *
     * @throws ClassDoesNotExist
     * @throws NoObjectProvided
     * @throws NotAnObject
     * @throws ObjectNotInstanceOfClass
     */
    public function setStaticPropertyValue(string $propertyName, $value) : void
    {
        $property = $this->getProperty($propertyName);

        if (! $property || ! $property->isStatic()) {
            throw PropertyDoesNotExist::fromName($propertyName);
        }

        $property->setValue($value);
    }

    /**
     * @return mixed[]
     */
    public function getStaticProperties() : array
    {
        $staticProperties = [];

        foreach ($this->getProperties() as $property) {
            if (! $property->isStatic()) {
                continue;
            }

            $staticProperties[$property->getName()] = $property->getValue();
        }

        return $staticProperties;
    }

    /**
     * Retrieve the AST node for this class
     */
    public function getAst() : ClassLikeNode
    {
        return $this->node;
    }

    public function getDeclaringNamespaceAst() : ?Namespace_
    {
        return $this->declaringNamespace;
    }

    /**
     * Set whether this class is final or not
     *
     * @throws NotAClassReflection
     */
    public function setFinal(bool $isFinal) : void
    {
        if (! $this->node instanceof ClassNode) {
            throw NotAClassReflection::fromReflectionClass($this);
        }

        if ($isFinal === true) {
            $this->node->flags |= ClassNode::MODIFIER_FINAL;

            return;
        }

        $this->node->flags &= ~ClassNode::MODIFIER_FINAL;
    }

    /**
     * Remove the named method from the class.
     *
     * Returns true if method was successfully removed.
     * Returns false if method was not found, or could not be removed.
     */
    public function removeMethod(string $methodName) : bool
    {
        $lowerName = strtolower($methodName);
        foreach ($this->node->stmts as $key => $stmt) {
            if ($stmt instanceof ClassMethod && $lowerName === $stmt->name->toLowerString()) {
                unset($this->node->stmts[$key], $this->cachedMethods);

                return true;
            }
        }

        return false;
    }

    /**
     * Add a new method to the class.
     */
    public function addMethod(string $methodName) : void
    {
        $this->node->stmts[] = new ClassMethod($methodName);
        unset($this->cachedMethods);
    }

    /**
     * Add a new property to the class.
     *
     * Visibility defaults to \ReflectionProperty::IS_PUBLIC, or can be ::IS_PROTECTED or ::IS_PRIVATE.
     */
    public function addProperty(
        string $propertyName,
        int $visibility = CoreReflectionProperty::IS_PUBLIC,
        bool $static = false
    ) : void {
        $type = 0;
        switch ($visibility) {
            case CoreReflectionProperty::IS_PRIVATE:
                $type |= ClassNode::MODIFIER_PRIVATE;
                break;
            case CoreReflectionProperty::IS_PROTECTED:
                $type |= ClassNode::MODIFIER_PROTECTED;
                break;
            default:
                $type |= ClassNode::MODIFIER_PUBLIC;
                break;
        }

        if ($static) {
            $type |= ClassNode::MODIFIER_STATIC;
        }

        $this->node->stmts[]             = new PropertyNode($type, [new Node\Stmt\PropertyProperty($propertyName)]);
        $this->cachedProperties          = null;
        $this->cachedImmediateProperties = null;
    }

    /**
     * Remove a property from the class.
     */
    public function removeProperty(string $propertyName) : bool
    {
        $lowerName = strtolower($propertyName);

        foreach ($this->node->stmts as $key => $stmt) {
            if (! ($stmt instanceof PropertyNode)) {
                continue;
            }

            $propertyNames = array_map(static function (Node\Stmt\PropertyProperty $propertyProperty) : string {
                return $propertyProperty->name->toLowerString();
            }, $stmt->props);

            if (in_array($lowerName, $propertyNames, true)) {
                $this->cachedProperties          = null;
                $this->cachedImmediateProperties = null;
                unset($this->node->stmts[$key]);

                return true;
            }
        }

        return false;
    }
}


<?php

declare(strict_types=1);

namespace Roave\BetterReflection\Identifier;

use InvalidArgumentException;
use Roave\BetterReflection\Reflection\Reflection;
use Roave\BetterReflection\Reflection\ReflectionClass;
use Roave\BetterReflection\Reflection\ReflectionFunction;
use function array_key_exists;
use function sprintf;

class IdentifierType
{
    public const IDENTIFIER_CLASS    = ReflectionClass::class;
    public const IDENTIFIER_FUNCTION = ReflectionFunction::class;

    private const VALID_TYPES = [
        self::IDENTIFIER_CLASS    => null,
        self::IDENTIFIER_FUNCTION => null,
    ];

    /** @var string */
    private $name;

    public function __construct(string $type = self::IDENTIFIER_CLASS)
    {
        if (! array_key_exists($type, self::VALID_TYPES)) {
            throw new InvalidArgumentException(sprintf(
                '%s is not a valid identifier type',
                $type
            ));
        }
        $this->name = $type;
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function isClass() : bool
    {
        return $this->name === self::IDENTIFIER_CLASS;
    }

    public function isFunction() : bool
    {
        return $this->name === self::IDENTIFIER_FUNCTION;
    }

    /**
     * Check to see if a reflector is of a valid type specified by this identifier.
     */
    public function isMatchingReflector(Reflection $reflector) : bool
    {
        if ($this->name === self::IDENTIFIER_CLASS) {
            return $reflector instanceof ReflectionClass;
        }

        if ($this->name === self::IDENTIFIER_FUNCTION) {
            return $reflector instanceof ReflectionFunction;
        }

        return false;
    }
}


<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\Ast\Parser;

use PhpParser\ErrorHandler;
use PhpParser\Node;
use PhpParser\Parser;
use function array_key_exists;
use function hash;
use function strlen;

/**
 * @internal
 */
final class MemoizingParser implements Parser
{
    /** @var Node\Stmt[][]|null[] indexed by source hash */
    private $sourceHashToAst = [];

    /** @var Parser */
    private $wrappedParser;

    public function __construct(Parser $wrappedParser)
    {
        $this->wrappedParser = $wrappedParser;
    }

    /**
     * {@inheritDoc}
     */
    public function parse(string $code, ?ErrorHandler $errorHandler = null) : ?array
    {
        // note: this code is mathematically buggy by default, as we are using a hash to identify
        //       cache entries. The string length is added to further reduce likeliness (although
        //       already imperceptible) of key collisions.
        //       In the "real world", this code will work just fine.
        $hash = hash('sha256', $code) . ':' . strlen($code);

        if (array_key_exists($hash, $this->sourceHashToAst)) {
            return $this->sourceHashToAst[$hash];
        }

        return $this->sourceHashToAst[$hash] = $this->wrappedParser->parse($code, $errorHandler);
    }
}


<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\Type;

use Roave\BetterReflection\Identifier\Identifier;
use Roave\BetterReflection\Identifier\IdentifierType;
use Roave\BetterReflection\Reflection\Reflection;
use Roave\BetterReflection\Reflector\Reflector;
use function array_map;
use function array_merge;

class AggregateSourceLocator implements SourceLocator
{
    /** @var SourceLocator[] */
    private $sourceLocators;

    /**
     * @param SourceLocator[] $sourceLocators
     */
    public function __construct(array $sourceLocators = [])
    {
        // This slightly confusing code simply type-checks the $sourceLocators
        // array by unpacking them and splatting them in the closure.
        $validator            = static function (SourceLocator ...$sourceLocator) : array {
            return $sourceLocator;
        };
        $this->sourceLocators = $validator(...$sourceLocators);
    }

    /**
     * {@inheritDoc}
     */
    public function locateIdentifier(Reflector $reflector, Identifier $identifier) : ?Reflection
    {
        foreach ($this->sourceLocators as $sourceLocator) {
            $located = $sourceLocator->locateIdentifier($reflector, $identifier);

            if ($located) {
                return $located;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType) : array
    {
        return array_merge(
            [],
            ...array_map(static function (SourceLocator $sourceLocator) use ($reflector, $identifierType) {
                return $sourceLocator->locateIdentifiersByType($reflector, $identifierType);
            }, $this->sourceLocators)
        );
    }
}


<?php

declare(strict_types=1);

namespace Roave\BetterReflection\SourceLocator\SourceStubber;

use DirectoryIterator;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\NameResolver;
use PhpParser\NodeVisitorAbstract;
use PhpParser\Parser;
use PhpParser\PrettyPrinter\Standard;
use ReflectionClass as CoreReflectionClass;
use ReflectionFunction as CoreReflectionFunction;
use Roave\BetterReflection\SourceLocator\FileChecker;
use Roave\BetterReflection\SourceLocator\SourceStubber\Exception\CouldNotFindPhpStormStubs;
use Traversable;
use function array_key_exists;
use function file_get_contents;
use function is_dir;
use function sprintf;
use function str_replace;

/**
 * @internal
 */
final class PhpStormStubsSourceStubber implements SourceStubber
{
    private const BUILDER_OPTIONS    = ['shortArraySyntax' => true];
    private const SEARCH_DIRECTORIES = [
        __DIR__ . '/../../../../../jetbrains/phpstorm-stubs',
        __DIR__ . '/../../../vendor/jetbrains/phpstorm-stubs',
    ];

    /** @var Parser */
    private $phpParser;

    /** @var Standard */
    private $prettyPrinter;

    /** @var NodeTraverser */
    private $nodeTraverser;

    /** @var string|null */
    private $stubsDirectory;

    /** @var string[][] */
    private $extensionStubsFiles = [];

    public function __construct(Parser $phpParser)
    {
        $this->phpParser     = $phpParser;
        $this->prettyPrinter = new Standard(self::BUILDER_OPTIONS);

        $this->nodeTraverser = new NodeTraverser();
        $this->nodeTraverser->addVisitor(new NameResolver());
    }

    /**
     * {@inheritDoc}
     */
    public function generateClassStub(CoreReflectionClass $classReflection) : ?string
    {
        if ($classReflection->isUserDefined()) {
            return null;
        }

        $stub = $this->getStub($classReflection->getExtensionName(), $this->getClassNodeVisitor($classReflection));

        if ($classReflection->getName() === Traversable::class) {
            // See https://github.com/JetBrains/phpstorm-stubs/commit/0778a26992c47d7dbee4d0b0bfb7fad4344371b1#diff-575bacb45377d474336c71cbf53c1729
            $stub = str_replace(' extends \iterable', '', $stub);
        }

        return $stub;
    }

    /**
     * {@inheritDoc}
     */
    public function generateFunctionStub(CoreReflectionFunction $functionReflection) : ?string
    {
        if ($functionReflection->isUserDefined()) {
            return null;
        }

        return $this->getStub($functionReflection->getExtensionName(), $this->getFunctionNodeVisitor($functionReflection));
    }

    private function getStub(string $extensionName, NodeVisitorAbstract $nodeVisitor) : ?string
    {
        $node = null;

        $this->nodeTraverser->addVisitor($nodeVisitor);

        foreach ($this->getExtensionStubsFiles($extensionName) as $filePath) {
            FileChecker::assertReadableFile($filePath);

            $ast = $this->phpParser->parse(file_get_contents($filePath));

            $this->nodeTraverser->traverse($ast);

            /** @psalm-suppress UndefinedMethod */
            $node = $nodeVisitor->getNode();
            if ($node !== null) {
                break;
            }
        }

        $this->nodeTraverser->removeVisitor($nodeVisitor);

        if ($node === null) {
            return null;
        }

        return "<?php\n\n" . $this->prettyPrinter->prettyPrint([$node]) . "\n";
    }

    private function getClassNodeVisitor(CoreReflectionClass $classReflection) : NodeVisitorAbstract
    {
        return new class($classReflection->getName()) extends NodeVisitorAbstract
        {
            /** @var string */
            private $className;

            /** @var Node\Stmt\ClassLike|null */
            private $node;

            public function __construct(string $className)
            {
                $this->className = $className;
            }

            public function enterNode(Node $node) : ?int
            {
                if ($node instanceof Node\Stmt\Namespace_) {
                    return null;
                }

                if ($node instanceof Node\Stmt\ClassLike && $node->namespacedName->toString() === $this->className) {
                    $this->node = $node;

                    return NodeTraverser::STOP_TRAVERSAL;
                }

                return NodeTraverser::DONT_TRAVERSE_CHILDREN;
            }

            public function getNode() : ?Node\Stmt\ClassLike
            {
                return $this->node;
            }
        };
    }

    private function getFunctionNodeVisitor(CoreReflectionFunction $functionReflection) : NodeVisitorAbstract
    {
        return new class($functionReflection->getName()) extends NodeVisitorAbstract
        {
            /** @var string */
            private $functionName;

            /** @var Node\Stmt\Function_|null */
            private $node;

            public function __construct(string $className)
            {
                $this->functionName = $className;
            }

            public function enterNode(Node $node) : ?int
            {
                if ($node instanceof Node\Stmt\Namespace_) {
                    return null;
                }

                /** @psalm-suppress UndefinedPropertyFetch */
                if ($node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $this->functionName) {
                    $this->node = $node;

                    return NodeTraverser::STOP_TRAVERSAL;
                }

                return NodeTraverser::DONT_TRAVERSE_CHILDREN;
            }

            public function getNode() : ?Node\Stmt\Function_
            {
                return $this->node;
            }
        };
    }

    /**
     * @return string[]
     */
    private function getExtensionStubsFiles(string $extensionName) : array
    {
        if (array_key_exists($extensionName, $this->extensionStubsFiles)) {
            return $this->extensionStubsFiles[$extensionName];
        }

        $this->extensionStubsFiles[$extensionName] = [];

        $extensionDirectory = sprintf('%s/%s', $this->getStubsDirectory(), $extensionName);

        if (! is_dir($extensionDirectory)) {
            return [];
        }

        foreach (new DirectoryIterator($extensionDirectory) as $fileInfo) {
            if ($fileInfo->isDot()) {
                continue;
            }

            $this->extensionStubsFiles[$extensionName][] = $fileInfo->getPathname();
        }

        return $this->extensionStubsFiles[$extensionName];
    }

    private function getStubsDirectory() : string
    {
        if ($this->stubsDirectory !== null) {
            return $this->stubsDirectory;
        }

        foreach (self::SEARCH_DIRECTORIES as $directory) {
            if (is_dir($directory)) {
                return $this->stubsDirectory = $directory;
            }
        }

        throw CouldNotFindPhpStormStubs::create();
    }
}


<?php

declare(strict_types=1);

namespace Roave\BetterReflection\Reflection;

use InvalidArgumentException;
use PhpParser\Builder\Property as PropertyNodeBuilder;
use PhpParser\Node\Stmt\ClassLike as ClassLikeNode;
use PhpParser\Node\Stmt\Namespace_;
use PhpParser\Node\Stmt\Property as PropertyNode;
use ReflectionException;
use ReflectionObject as CoreReflectionObject;
use ReflectionProperty as CoreReflectionProperty;
use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflector\ClassReflector;
use Roave\BetterReflection\Reflector\Exception\IdentifierNotFound;
use Roave\BetterReflection\Reflector\Reflector;
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
use Roave\BetterReflection\SourceLocator\Type\AnonymousClassObjectSourceLocator;
use function array_merge;
use function get_class;
use function is_object;
use function strpos;

class ReflectionObject extends ReflectionClass
{
    /** @var ReflectionClass */
    private $reflectionClass;

    /** @var object */
    private $object;

    /** @var Reflector */
    private $reflector;

    /**
     * @param object $object
     */
    private function __construct(Reflector $reflector, ReflectionClass $reflectionClass, $object)
    {
        $this->reflector       = $reflector;
        $this->reflectionClass = $reflectionClass;
        $this->object          = $object;
    }

    /**
     * Create a reflection and return the string representation of a class instance
     *
     * @param object $instance
     *
     * @throws IdentifierNotFound
     * @throws ReflectionException
     * @throws InvalidArgumentException
     */
    public static function export($instance = null) : string
    {
        if ($instance === null) {
            throw new InvalidArgumentException('Class instance must be provided');
        }

        return self::createFromInstance($instance)->__toString();
    }

    /**
     * Pass an instance of an object to this method to reflect it
     *
     * @param object $object
     *
     * @throws ReflectionException
     * @throws InvalidArgumentException
     * @throws IdentifierNotFound
     *
     * @psalm-suppress DocblockTypeContradiction
     */
    public static function createFromInstance($object) : ReflectionClass
    {
        if (! is_object($object)) {
            throw new InvalidArgumentException('Can only create from an instance of an object');
        }

        $className = get_class($object);

        if (strpos($className, ReflectionClass::ANONYMOUS_CLASS_NAME_PREFIX) === 0) {
            $reflector = new ClassReflector(new AnonymousClassObjectSourceLocator(
                $object,
                (new BetterReflection())->phpParser()
            ));
        } else {
            $reflector = (new BetterReflection())->classReflector();
        }

        return new self($reflector, $reflector->reflect($className), $object);
    }

    /**
     * Reflect on runtime properties for the current instance
     *
     * @see ReflectionClass::getProperties() for the usage of $filter
     *
     * @return ReflectionProperty[]
     */
    private function getRuntimeProperties(?int $filter = null) : array
    {
        if (! $this->reflectionClass->isInstance($this->object)) {
            throw new InvalidArgumentException('Cannot reflect runtime properties of a separate class');
        }

        // Ensure we have already cached existing properties so we can add to them
        $this->reflectionClass->getProperties();

        // Only known current way is to use internal ReflectionObject to get
        // the runtime-declared properties  :/
        $reflectionProperties = (new CoreReflectionObject($this->object))->getProperties();
        $runtimeProperties    = [];

        foreach ($reflectionProperties as $property) {
            if ($this->reflectionClass->hasProperty($property->getName())) {
                continue;
            }

            $reflectionProperty = $this->reflectionClass->getProperty($property->getName());

            $runtimeProperty = ReflectionProperty::createFromNode(
                $this->reflector,
                $this->createPropertyNodeFromReflection($property, $this->object),
                0,
                $reflectionProperty
                    ? $reflectionProperty->getDeclaringClass()->getDeclaringNamespaceAst()
                    : null,
                $this,
                $this,
                false
            );

            if ($filter !== null && ! ($filter & $runtimeProperty->getModifiers())) {
                continue;
            }

            $runtimeProperties[$runtimeProperty->getName()] = $runtimeProperty;
        }

        return $runtimeProperties;
    }

    /**
     * Create an AST PropertyNode given a reflection
     *
     * Note that we don't copy across DocBlock, protected, private or static
     * because runtime properties can't have these attributes.
     *
     * @param object $instance
     */
    private function createPropertyNodeFromReflection(CoreReflectionProperty $property, $instance) : PropertyNode
    {
        $builder = new PropertyNodeBuilder($property->getName());
        $builder->setDefault($property->getValue($instance));

        if ($property->isPublic()) {
            $builder->makePublic();
        }

        return $builder->getNode();
    }

    /**
     * {@inheritdoc}
     */
    public function getShortName() : string
    {
        return $this->reflectionClass->getShortName();
    }

    /**
     * {@inheritdoc}
     */
    public function getName() : string
    {
        return $this->reflectionClass->getName();
    }

    /**
     * {@inheritdoc}
     */
    public function getNamespaceName() : string
    {
        return $this->reflectionClass->getNamespaceName();
    }

    /**
     * {@inheritdoc}
     */
    public function inNamespace() : bool
    {
        return $this->reflectionClass->inNamespace();
    }

    public function getExtensionName() : ?string
    {
        return $this->reflectionClass->getExtensionName();
    }

    /**
     * {@inheritdoc}
     */
    public function getMethods(?int $filter = null) : array
    {
        return $this->reflectionClass->getMethods($filter);
    }

    /**
     * {@inheritdoc}
     */
    public function getImmediateMethods(?int $filter = null) : array
    {
        return $this->reflectionClass->getImmediateMethods($filter);
    }

    /**
     * {@inheritdoc}
     */
    public function getMethod(string $methodName) : ReflectionMethod
    {
        return $this->reflectionClass->getMethod($methodName);
    }

    /**
     * {@inheritdoc}
     */
    public function hasMethod(string $methodName) : bool
    {
        return $this->reflectionClass->hasMethod($methodName);
    }

    /**
     * {@inheritdoc}
     */
    public function getImmediateConstants() : array
    {
        return $this->reflectionClass->getImmediateConstants();
    }

    /**
     * {@inheritdoc}
     */
    public function getConstants() : array
    {
        return $this->reflectionClass->getConstants();
    }

    /**
     * {@inheritdoc}
     */
    public function getConstant(string $name)
    {
        return $this->reflectionClass->getConstant($name);
    }

    /**
     * {@inheritdoc}
     */
    public function hasConstant(string $name) : bool
    {
        return $this->reflectionClass->hasConstant($name);
    }

    /**
     * {@inheritdoc}
     */
    public function getReflectionConstant(string $name) : ?ReflectionClassConstant
    {
        return $this->reflectionClass->getReflectionConstant($name);
    }

    /**
     * {@inheritdoc}
     */
    public function getImmediateReflectionConstants() : array
    {
        return $this->reflectionClass->getImmediateReflectionConstants();
    }

    /**
     * {@inheritdoc}
     */
    public function getReflectionConstants() : array
    {
        return $this->reflectionClass->getReflectionConstants();
    }

    /**
     * {@inheritdoc}
     */
    public function getConstructor() : ReflectionMethod
    {
        return $this->reflectionClass->getConstructor();
    }

    /**
     * {@inheritdoc}
     */
    public function getProperties(?int $filter = null) : array
    {
        return array_merge(
            $this->reflectionClass->getProperties($filter),
            $this->getRuntimeProperties($filter)
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getImmediateProperties(?int $filter = null) : array
    {
        return array_merge(
            $this->reflectionClass->getImmediateProperties($filter),
            $this->getRuntimeProperties($filter)
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getProperty(string $name) : ?ReflectionProperty
    {
        $runtimeProperties = $this->getRuntimeProperties();

        if (isset($runtimeProperties[$name])) {
            return $runtimeProperties[$name];
        }

        return $this->reflectionClass->getProperty($name);
    }

    /**
     * {@inheritdoc}
     */
    public function hasProperty(string $name) : bool
    {
        $runtimeProperties = $this->getRuntimeProperties();

        return isset($runtimeProperties[$name]) || $this->reflectionClass->hasProperty($name);
    }

    /**
     * {@inheritdoc}
     */
    public function getDefaultProperties() : array
    {
        return $this->reflectionClass->getDefaultProperties();
    }

    /**
     * {@inheritdoc}
     */
    public function getFileName() : ?string
    {
        return $this->reflectionClass->getFileName();
    }

    /**
     * {@inheritdoc}
     */
    public function getLocatedSource() : LocatedSource
    {
        return $this->reflectionClass->getLocatedSource();
    }

    /**
     * {@inheritdoc}
     */
    public function getStartLine() : int
    {
        return $this->reflectionClass->getStartLine();
    }

    /**
     * {@inheritdoc}
     */
    public function getEndLine() : int
    {
        return $this->reflectionClass->getEndLine();
    }

    /**
     * {@inheritdoc}
     */
    public function getStartColumn() : int
    {
        return $this->reflectionClass->getStartColumn();
    }

    /**
     * {@inheritdoc}
     */
    public function getEndColumn() : int
    {
        return $this->reflectionClass->getEndColumn();
    }

    /**
     * {@inheritdoc}
     */
    public function getParentClass() : ?ReflectionClass
    {
        return $this->reflectionClass->getParentClass();
    }

    /**
     * {@inheritdoc}
     */
    public function getParentClassNames() : array
    {
        return $this->reflectionClass->getParentClassNames();
    }

    /**
     * {@inheritdoc}
     */
    public function getDocComment() : string
    {
        return $this->reflectionClass->getDocComment();
    }

    /**
     * {@inheritdoc}
     */
    public function isAnonymous() : bool
    {
        return $this->reflectionClass->isAnonymous();
    }

    /**
     * {@inheritdoc}
     */
    public function isInternal() : bool
    {
        return $this->reflectionClass->isInternal();
    }

    /**
     * {@inheritdoc}
     */
    public function isUserDefined() : bool
    {
        return $this->reflectionClass->isUserDefined();
    }

    /**
     * {@inheritdoc}
     */
    public function isAbstract() : bool
    {
        return $this->reflectionClass->isAbstract();
    }

    /**
     * {@inheritdoc}
     */
    public function isFinal() : bool
    {
        return $this->reflectionClass->isFinal();
    }

    /**
     * {@inheritdoc}
     */
    public function getModifiers() : int
    {
        return $this->reflectionClass->getModifiers();
    }

    /**
     * {@inheritdoc}
     */
    public function isTrait() : bool
    {
        return $this->reflectionClass->isTrait();
    }

    /**
     * {@inheritdoc}
     */
    public function isInterface() : bool
    {
        return $this->reflectionClass->isInterface();
    }

    /**
     * {@inheritdoc}
     */
    public function getTraits() : array
    {
        return $this->reflectionClass->getTraits();
    }

    /**
     * {@inheritdoc}
     */
    public function getTraitNames() : array
    {
        return $this->reflectionClass->getTraitNames();
    }

    /**
     * {@inheritdoc}
     */
    public function getTraitAliases() : array
    {
        return $this->reflectionClass->getTraitAliases();
    }

    /**
     * {@inheritdoc}
     */
    public function getInterfaces() : array
    {
        return $this->reflectionClass->getInterfaces();
    }

    /**
     * {@inheritdoc}
     */
    public function getImmediateInterfaces() : array
    {
        return $this->reflectionClass->getImmediateInterfaces();
    }

    /**
     * {@inheritdoc}
     */
    public function getInterfaceNames() : array
    {
        return $this->reflectionClass->getInterfaceNames();
    }

    /**
     * {@inheritdoc}
     */
    public function isInstance($object) : bool
    {
        return $this->reflectionClass->isInstance($object);
    }

    /**
     * {@inheritdoc}
     */
    public function isSubclassOf(string $className) : bool
    {
        return $this->reflectionClass->isSubclassOf($className);
    }

    /**
     * {@inheritdoc}
     */
    public function implementsInterface(string $interfaceName) : bool
    {
        return $this->reflectionClass->implementsInterface($interfaceName);
    }

    /**
     * {@inheritdoc}
     */
    public function isInstantiable() : bool
    {
        return $this->reflectionClass->isInstantiable();
    }

    /**
     * {@inheritdoc}
     */
    public function isCloneable() : bool
    {
        return $this->reflectionClass->isCloneable();
    }

    /**
     * {@inheritdoc}
     */
    public function isIterateable() : bool
    {
        return $this->reflectionClass->isIterateable();
    }

    /**
     * {@inheritdoc}
     */
    public function getStaticProperties() : array
    {
        return $this->reflectionClass->getStaticProperties();
    }

    /**
     * {@inheritdoc}
     */
    public function setStaticPropertyValue(string $propertyName, $value) : void
    {
        $this->reflectionClass->setStaticPropertyValue($propertyName, $value);
    }

    /**
     * {@inheritdoc}
     */
    public function getStaticPropertyValue(string $propertyName)
    {
        return $this->reflectionClass->getStaticPropertyValue($propertyName);
    }

    /**
     * {@inheritdoc}
     */
    public function getAst() : ClassLikeNode
    {
        return $this->reflectionClass->getAst();
    }

    /**
     * {@inheritdoc}
     */
    public function getDeclaringNamespaceAst() : ?Namespace_
    {
        return $this->reflectionClass->getDeclaringNamespaceAst();
    }

    /**
     * {@inheritdoc}
     */
    public function setFinal(bool $isFinal) : void
    {
        $this->reflectionClass->setFinal($isFinal);
    }

    /**
     * {@inheritdoc}
     */
    public function removeMethod(string $methodName) : bool
    {
        return $this->reflectionClass->removeMethod($methodName);
    }

    /**
     * {@inheritdoc}
     */
    public function addMethod(string $methodName) : void
    {
        $this->reflectionClass->addMethod($methodName);
    }

    /**
     * {@inheritdoc}
     */
    public function removeProperty(string $methodName) : bool
    {
        return $this->reflectionClass->removeProperty($methodName);
    }

    /**
     * {@inheritdoc}
     */
    public function addProperty(
        string $methodName,
        int $visibility = CoreReflectionProperty::IS_PUBLIC,
        bool $static = false
    ) : void {
        $this->reflectionClass->addProperty($methodName, $visibility, $static);
    }
}


<?php

declare(strict_types=1);

namespace Roave\BetterReflection\Reflection\Adapter;

use ReflectionException as CoreReflectionException;
use ReflectionObject as CoreReflectionObject;
use Roave\BetterReflection\Reflection\ReflectionMethod as BetterReflectionMethod;
use Roave\BetterReflection\Reflection\ReflectionObject as BetterReflectionObject;
use function array_combine;
use function array_map;
use function func_get_args;
use function func_num_args;
use function sprintf;
use function strtolower;

class ReflectionObject extends CoreReflectionObject
{
    /** @var BetterReflectionObject */
    private $betterReflectionObject;

    public function __construct(BetterReflectionObject $betterReflectionObject)
    {
        $this->betterReflectionObject = $betterReflectionObject;
    }

    /**
     * {@inheritDoc}
     */
    public static function export($argument, $return = null)
    {
        return BetterReflectionObject::export(...func_get_args());
    }

    /**
     * {@inheritDoc}
     */
    public function __toString()
    {
        return $this->betterReflectionObject->__toString();
    }

    /**
     * {@inheritDoc}
     */
    public function getName()
    {
        return $this->betterReflectionObject->getName();
    }

    /**
     * {@inheritDoc}
     */
    public function isInternal()
    {
        return $this->betterReflectionObject->isInternal();
    }

    /**
     * {@inheritDoc}
     */
    public function isUserDefined()
    {
        return $this->betterReflectionObject->isUserDefined();
    }

    /**
     * {@inheritDoc}
     */
    public function isInstantiable()
    {
        return $this->betterReflectionObject->isInstantiable();
    }

    /**
     * {@inheritDoc}
     */
    public function isCloneable()
    {
        return $this->betterReflectionObject->isCloneable();
    }

    /**
     * {@inheritDoc}
     */
    public function getFileName()
    {
        return $this->betterReflectionObject->getFileName() ?? false;
    }

    /**
     * {@inheritDoc}
     */
    public function getStartLine()
    {
        return $this->betterReflectionObject->getStartLine();
    }

    /**
     * {@inheritDoc}
     */
    public function getEndLine()
    {
        return $this->betterReflectionObject->getEndLine();
    }

    /**
     * {@inheritDoc}
     */
    public function getDocComment()
    {
        return $this->betterReflectionObject->getDocComment() ?: false;
    }

    /**
     * {@inheritDoc}
     */
    public function getConstructor()
    {
        return new ReflectionMethod($this->betterReflectionObject->getConstructor());
    }

    /**
     * {@inheritDoc}
     */
    public function hasMethod($name)
    {
        return $this->betterReflectionObject->hasMethod($this->getMethodRealName($name));
    }

    /**
     * {@inheritDoc}
     */
    public function getMethod($name)
    {
        return new ReflectionMethod($this->betterReflectionObject->getMethod($this->getMethodRealName($name)));
    }

    private function getMethodRealName(string $name) : string
    {
        $realMethodNames = array_map(static function (BetterReflectionMethod $method) : string {
            return $method->getName();
        }, $this->betterReflectionObject->getMethods());

        $methodNames = array_combine(array_map(static function (string $methodName) : string {
            return strtolower($methodName);
        }, $realMethodNames), $realMethodNames);

        return $methodNames[strtolower($name)] ?? $name;
    }

    /**
     * {@inheritDoc}
     */
    public function getMethods($filter = null)
    {
        $methods = $this->betterReflectionObject->getMethods();

        $wrappedMethods = [];
        foreach ($methods as $key => $method) {
            $wrappedMethods[$key] = new ReflectionMethod($method);
        }

        return $wrappedMethods;
    }

    /**
     * {@inheritDoc}
     */
    public function hasProperty($name)
    {
        return $this->betterReflectionObject->hasProperty($name);
    }

    /**
     * {@inheritDoc}
     */
    public function getProperty($name)
    {
        $property = $this->betterReflectionObject->getProperty($name);

        if ($property === null) {
            throw new CoreReflectionException(sprintf('Property "%s" does not exist', $name));
        }

        return new ReflectionProperty($property);
    }

    /**
     * {@inheritDoc}
     */
    public function getProperties($filter = null)
    {
        $properties = $this->betterReflectionObject->getProperties();

        $wrappedProperties = [];
        foreach ($properties as $key => $property) {
            $wrappedProperties[$key] = new ReflectionProperty($property);
        }

        return $wrappedProperties;
    }

    /**
     * {@inheritDoc}
     */
    public function hasConstant($name)
    {
        return $this->betterReflectionObject->hasConstant($name);
    }

    /**
     * {@inheritDoc}
     */
    public function getConstants()
    {
        return $this->betterReflectionObject->getConstants();
    }

    /**
     * {@inheritDoc}
     */
    public function getConstant($name)
    {
        return $this->betterReflectionObject->getConstant($name);
    }

    /**
     * {@inheritDoc}
     */
    public function getInterfaces()
    {
        $interfaces = $this->betterReflectionObject->getInterfaces();

        $wrappedInterfaces = [];
        foreach ($interfaces as $key => $interface) {
            $wrappedInterfaces[$key] = new ReflectionClass($interface);
        }

        return $wrappedInterfaces;
    }

    /**
     * {@inheritDoc}
     */
    public function getInterfaceNames()
    {
        return $this->betterReflectionObject->getInterfaceNames();
    }

    /**
     * {@inheritDoc}
     */
    public function isInterface()
    {
        return $this->betterReflectionObject->isInterface();
    }

    /**
     * {@inheritDoc}
     */
    public function getTraits()
    {
        $traits = $this->betterReflectionObject->getTraits();

        $wrappedTraits = [];
        foreach ($traits as $key => $trait) {
            $wrappedTraits[$key] = new ReflectionClass($trait);
        }

        return $wrappedTraits;
    }

    /**
     * {@inheritDoc}
     */
    public function getTraitNames()
    {
        return $this->betterReflectionObject->getTraitNames();
    }

    /**
     * {@inheritDoc}
     */
    public function getTraitAliases()
    {
        return $this->betterReflectionObject->getTraitAliases();
    }

    /**
     * {@inheritDoc}
     */
    public function isTrait()
    {
        return $this->betterReflectionObject->isTrait();
    }

    /**
     * {@inheritDoc}
     */
    public function isAbstract()
    {
        return $this->betterReflectionObject->isAbstract();
    }

    /**
     * {@inheritDoc}
     */
    public function isFinal()
    {
        return $this->betterReflectionObject->isFinal();
    }

    /**
     * {@inheritDoc}
     */
    public function getModifiers()
    {
        return $this->betterReflectionObject->getModifiers();
    }

    /**
     * {@inheritDoc}
     */
    public function isInstance($object)
    {
        return $this->betterReflectionObject->isInstance($object);
    }

    /**
     * {@inheritDoc}
     */
    public function newInstance($arg = null, ...$args)
    {
        throw new Exception\NotImplemented('Not implemented');
    }

    /**
     * {@inheritDoc}
     */
    public function newInstanceWithoutConstructor()
    {
        throw new Exception\NotImplemented('Not implemented');
    }

    /**
     * {@inheritDoc}
     */
    public function newInstanceArgs(?array $args = null)
    {
        throw new Exception\NotImplemented('Not implemented');
    }

    /**
     * {@inheritDoc}
     */
    public function getParentClass()
    {
        $parentClass = $this->betterReflectionObject->getParentClass();

        if ($parentClass === null) {
            return false;
        }

        return new ReflectionClass($parentClass);
    }

    /**
     * {@inheritDoc}
     */
    public function isSubclassOf($class)
    {
        $realParentClassNames = $this->betterReflectionObject->getParentClassNames();

        $parentClassNames = array_combine(array_map(static function (string $parentClassName) : string {
            return strtolower($parentClassName);
        }, $realParentClassNames), $realParentClassNames);

        $realParentClassName = $parentClassNames[strtolower($class)] ?? $class;

        return $this->betterReflectionObject->isSubclassOf($realParentClassName);
    }

    /**
     * {@inheritDoc}
     */
    public function getStaticProperties()
    {
        return $this->betterReflectionObject->getStaticProperties();
    }

    /**
     * {@inheritDoc}
     */
    public function getStaticPropertyValue($name, $default = null)
    {
        $betterReflectionProperty = $this->betterReflectionObject->getProperty($name);

        if ($betterReflectionProperty === null) {
            if (func_num_args() === 2) {
                return $default;
            }

            throw new CoreReflectionException(sprintf('Property "%s" does not exist', $name));
        }

        $property = new ReflectionProperty($betterReflectionProperty);

        if (! $property->isAccessible()) {
            throw new CoreReflectionException(sprintf('Property "%s" is not accessible', $name));
        }

        if (! $property->isStatic()) {
            throw new CoreReflectionException(sprintf('Property "%s" is not static', $name));
        }

        return $property->getValue();
    }

    /**
     * {@inheritDoc}
     */
    public function setStaticPropertyValue($name, $value)
    {
        $betterReflectionProperty = $this->betterReflectionObject->getProperty($name);

        if ($betterReflectionProperty === null) {
            throw new CoreReflectionException(sprintf('Property "%s" does not exist', $name));
        }

        $property = new ReflectionProperty($betterReflectionProperty);

        if (! $property->isAccessible()) {
            throw new CoreReflectionException(sprintf('Property "%s" is not accessible', $name));
        }

        if (! $property->isStatic()) {
            throw new CoreReflectionException(sprintf('Property "%s" is not static', $name));
        }

        $property->setValue($value);
    }

    /**
     * {@inheritDoc}
     */
    public function getDefaultProperties()
    {
        return $this->betterReflectionObject->getDefaultProperties();
    }

    /**
     * {@inheritDoc}
     */
    public function isIterateable()
    {
        return $this->betterReflectionObject->isIterateable();
    }

    /**
     * {@inheritDoc}
     */
    public function implementsInterface($interface)
    {
        $realInterfaceNames = $this->betterReflectionObject->getInterfaceNames();

        $interfaceNames = array_combine(array_map(static function (string $interfaceName) : string {
            return strtolower($interfaceName);
        }, $realInterfaceNames), $realInterfaceNames);

        $realInterfaceName = $interfaceNames[strtolower($interface)] ?? $interface;

        return $this->betterReflectionObject->implementsInterface($realInterfaceName);
    }

    /**
     * {@inheritDoc}
     */
    public function getExtension()
    {
        throw new Exception\NotImplemented('Not implemented');
    }

    /**
     * {@inheritDoc}
     */
    public function getExtensionName()
    {
        return $this->betterReflectionObject->getExtensionName() ?? false;
    }

    /**
     * {@inheritDoc}
     */
    public function inNamespace()
    {
        return $this->betterReflectionObject->inNamespace();
    }

    /**
     * {@inheritDoc}
     */
    public function getNamespaceName()
    {
        return $this->betterReflectionObject->getNamespaceName();
    }

    /**
     * {@inheritDoc}
     */
    public function getShortName()
    {
        return $this->betterReflectionObject->getShortName();
    }
}


<?php

declare(strict_types=1);

namespace Roave\BetterReflection\Reflection;

use Closure;
use Exception;
use LogicException;
use phpDocumentor\Reflection\Type;
use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Expr\Yield_ as YieldNode;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param as ParamNode;
use PhpParser\Node\Stmt\Namespace_ as NamespaceNode;
use PhpParser\NodeTraverser;
use PhpParser\Parser;
use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter;
use PhpParser\PrettyPrinterAbstract;
use Reflector as CoreReflector;
use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Identifier\Exception\InvalidIdentifierName;
use Roave\BetterReflection\Identifier\Identifier;
use Roave\BetterReflection\Identifier\IdentifierType;
use Roave\BetterReflection\Reflection\Exception\InvalidAbstractFunctionNodeType;
use Roave\BetterReflection\Reflection\Exception\Uncloneable;
use Roave\BetterReflection\Reflector\Reflector;
use Roave\BetterReflection\SourceLocator\Ast\Exception\ParseToAstFailure;
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
use Roave\BetterReflection\SourceLocator\Type\ClosureSourceLocator;
use Roave\BetterReflection\TypesFinder\FindReturnType;
use Roave\BetterReflection\Util\CalculateReflectionColum;
use Roave\BetterReflection\Util\GetFirstDocComment;
use Roave\BetterReflection\Util\Visitor\ReturnNodeVisitor;
use function array_filter;
use function count;
use function implode;
use function is_array;
use function is_string;
use function strtolower;

abstract class ReflectionFunctionAbstract implements CoreReflector
{
    public const CLOSURE_NAME = '{closure}';

    /** @var NamespaceNode|null */
    private $declaringNamespace;

    /** @var LocatedSource */
    private $locatedSource;

    /** @var Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure */
    private $node;

    /** @var Reflector */
    private $reflector;

    /** @var Parser */
    private static $parser;

    protected function __construct()
    {
    }

    public static function export() : void
    {
        throw new Exception('Unable to export statically');
    }

    /**
     * Populate the common elements of the function abstract.
     *
     * @param Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
     *
     * @throws InvalidAbstractFunctionNodeType
     */
    protected function populateFunctionAbstract(
        Reflector $reflector,
        Node\FunctionLike $node,
        LocatedSource $locatedSource,
        ?NamespaceNode $declaringNamespace = null
    ) : void {
        $this->reflector          = $reflector;
        $this->node               = $node;
        $this->locatedSource      = $locatedSource;
        $this->declaringNamespace = $declaringNamespace;

        $this->setNodeOptionalFlag();
    }

    /**
     * Get the AST node from which this function was created
     *
     * @return Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\FunctionLike
     */
    protected function getNode() : Node\FunctionLike
    {
        return $this->node;
    }

    /**
     * We must determine if params are optional or not ahead of time, but we
     * must do it in reverse...
     */
    private function setNodeOptionalFlag() : void
    {
        $overallOptionalFlag = true;
        $lastParamIndex      = count($this->node->params) - 1;
        for ($i = $lastParamIndex; $i >= 0; $i--) {
            $hasDefault = ($this->node->params[$i]->default !== null);

            // When we find the first parameter that does not have a default,
            // flip the flag as all params for this are no longer optional
            // EVEN if they have a default value
            if (! $hasDefault) {
                $overallOptionalFlag = false;
            }

            $this->node->params[$i]->isOptional = $overallOptionalFlag;
        }
    }

    /**
     * Get the "full" name of the function (e.g. for A\B\foo, this will return
     * "A\B\foo").
     */
    public function getName() : string
    {
        if (! $this->inNamespace()) {
            return $this->getShortName();
        }

        return $this->getNamespaceName() . '\\' . $this->getShortName();
    }

    /**
     * Get the "short" name of the function (e.g. for A\B\foo, this will return
     * "foo").
     */
    public function getShortName() : string
    {
        if ($this->node instanceof Node\Expr\Closure) {
            return self::CLOSURE_NAME;
        }

        return $this->node->name->name;
    }

    /**
     * Get the "namespace" name of the function (e.g. for A\B\foo, this will
     * return "A\B").
     *
     * @psalm-suppress PossiblyNullPropertyFetch
     */
    public function getNamespaceName() : string
    {
        if (! $this->inNamespace()) {
            return '';
        }

        return implode('\\', $this->declaringNamespace->name->parts);
    }

    /**
     * Decide if this function is part of a namespace. Returns false if the class
     * is in the global namespace or does not have a specified namespace.
     */
    public function inNamespace() : bool
    {
        return $this->declaringNamespace !== null
            && $this->declaringNamespace->name !== null;
    }

    /**
     * Get the number of parameters for this class.
     */
    public function getNumberOfParameters() : int
    {
        return count($this->getParameters());
    }

    /**
     * Get the number of required parameters for this method.
     */
    public function getNumberOfRequiredParameters() : int
    {
        return count(array_filter(
            $this->getParameters(),
            static function (ReflectionParameter $p) : bool {
                return ! $p->isOptional();
            }
        ));
    }

    /**
     * Get an array list of the parameters for this method signature, as an
     * array of ReflectionParameter instances.
     *
     * @return ReflectionParameter[]
     */
    public function getParameters() : array
    {
        $parameters = [];

        foreach ($this->node->params as $paramIndex => $paramNode) {
            $parameters[] = ReflectionParameter::createFromNode(
                $this->reflector,
                $paramNode,
                $this->declaringNamespace,
                $this,
                $paramIndex
            );
        }

        return $parameters;
    }

    /**
     * Get a single parameter by name. Returns null if parameter not found for
     * the function.
     */
    public function getParameter(string $parameterName) : ?ReflectionParameter
    {
        foreach ($this->getParameters() as $parameter) {
            if ($parameter->getName() === $parameterName) {
                return $parameter;
            }
        }

        return null;
    }

    public function getDocComment() : string
    {
        return GetFirstDocComment::forNode($this->node);
    }

    public function setDocCommentFromString(string $string) : void
    {
        $this->getAst()->setDocComment(new Doc($string));
    }

    public function getFileName() : ?string
    {
        return $this->locatedSource->getFileName();
    }

    public function getLocatedSource() : LocatedSource
    {
        return $this->locatedSource;
    }

    /**
     * Is this function a closure?
     */
    public function isClosure() : bool
    {
        return $this->node instanceof Node\Expr\Closure;
    }

    /**
     * Is this function deprecated?
     *
     * Note - we cannot reflect on internal functions (as there is no PHP source
     * code we can access. This means, at present, we can only EVER return false
     * from this function.
     *
     * @see https://github.com/Roave/BetterReflection/issues/38
     */
    public function isDeprecated() : bool
    {
        return false;
    }

    public function isInternal() : bool
    {
        return $this->locatedSource->isInternal();
    }

    /**
     * Is this a user-defined function (will always return the opposite of
     * whatever isInternal returns).
     */
    public function isUserDefined() : bool
    {
        return ! $this->isInternal();
    }

    public function getExtensionName() : ?string
    {
        return $this->locatedSource->getExtensionName();
    }

    /**
     * Check if the function has a variadic parameter.
     */
    public function isVariadic() : bool
    {
        $parameters = $this->getParameters();

        foreach ($parameters as $parameter) {
            if ($parameter->isVariadic()) {
                return true;
            }
        }

        return false;
    }

    /**
     * Recursively search an array of statements (PhpParser nodes) to find if a
     * yield expression exists anywhere (thus indicating this is a generator).
     */
    private function nodeIsOrContainsYield(Node $node) : bool
    {
        if ($node instanceof YieldNode) {
            return true;
        }

        foreach ($node->getSubNodeNames() as $nodeName) {
            $nodeProperty = $node->$nodeName;

            if ($nodeProperty instanceof Node && $this->nodeIsOrContainsYield($nodeProperty)) {
                return true;
            }

            if (! is_array($nodeProperty)) {
                continue;
            }

            foreach ($nodeProperty as $nodePropertyArrayItem) {
                if ($nodePropertyArrayItem instanceof Node && $this->nodeIsOrContainsYield($nodePropertyArrayItem)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Check if this function can be used as a generator (i.e. contains the
     * "yield" keyword).
     */
    public function isGenerator() : bool
    {
        if ($this->node === null) {
            return false;
        }

        return $this->nodeIsOrContainsYield($this->node);
    }

    /**
     * Get the line number that this function starts on.
     */
    public function getStartLine() : int
    {
        return $this->node->getStartLine();
    }

    /**
     * Get the line number that this function ends on.
     */
    public function getEndLine() : int
    {
        return $this->node->getEndLine();
    }

    public function getStartColumn() : int
    {
        return CalculateReflectionColum::getStartColumn($this->locatedSource->getSource(), $this->node);
    }

    public function getEndColumn() : int
    {
        return CalculateReflectionColum::getEndColumn($this->locatedSource->getSource(), $this->node);
    }

    /**
     * Is this function declared as a reference.
     */
    public function returnsReference() : bool
    {
        return $this->node->byRef;
    }

    /**
     * Get the return types defined in the DocBlocks. This returns an array because
     * the parameter may have multiple (compound) types specified (for example
     * when you type hint pipe-separated "string|null", in which case this
     * would return an array of Type objects, one for string, one for null.
     *
     * @return Type[]
     */
    public function getDocBlockReturnTypes() : array
    {
        return (new FindReturnType())->__invoke($this, $this->declaringNamespace);
    }

    /**
     * Get the return type declaration (only for PHP 7+ code)
     */
    public function getReturnType() : ?ReflectionType
    {
        $returnType = $this->node->getReturnType();

        if ($returnType === null) {
            return null;
        }

        if ($returnType instanceof NullableType) {
            return ReflectionType::createFromTypeAndReflector((string) $returnType->type, true, $this->reflector);
        }

        return ReflectionType::createFromTypeAndReflector((string) $returnType, false, $this->reflector);
    }

    /**
     * Do we have a return type declaration (only for PHP 7+ code)
     */
    public function hasReturnType() : bool
    {
        return $this->getReturnType() !== null;
    }

    /**
     * Set the return type declaration.
     */
    public function setReturnType(string $newReturnType) : void
    {
        $this->node->returnType = new Node\Name($newReturnType);
    }

    /**
     * Remove the return type declaration completely.
     */
    public function removeReturnType() : void
    {
        $this->node->returnType = null;
    }

    /**
     * @throws Uncloneable
     */
    public function __clone()
    {
        throw Uncloneable::fromClass(self::class);
    }

    /**
     * Retrieves the body of this function as AST nodes
     *
     * @return Node[]
     */
    public function getBodyAst() : array
    {
        return $this->node->stmts ?: [];
    }

    /**
     * Retrieves the body of this function as code.
     *
     * If a PrettyPrinter is provided as a parameter, it will be used, otherwise
     * a default will be used.
     *
     * Note that the formatting of the code may not be the same as the original
     * function. If specific formatting is required, you should provide your
     * own implementation of a PrettyPrinter to unparse the AST.
     */
    public function getBodyCode(?PrettyPrinterAbstract $printer = null) : string
    {
        if ($printer === null) {
            $printer = new StandardPrettyPrinter();
        }

        return $printer->prettyPrint($this->getBodyAst());
    }

    /**
     * Fetch the AST for this method or function.
     *
     * @return Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\FunctionLike
     */
    public function getAst() : Node\FunctionLike
    {
        return $this->node;
    }

    /**
     * Override the method or function's body of statements with an entirely new
     * body of statements within the reflection.
     *
     * @throws ParseToAstFailure
     * @throws InvalidIdentifierName
     *
     * @example
     * $reflectionFunction->setBodyFromClosure(function () { return true; });
     */
    public function setBodyFromClosure(Closure $newBody) : void
    {
        /** @var self $closureReflection */
        $closureReflection = (new ClosureSourceLocator($newBody, $this->loadStaticParser()))->locateIdentifier(
            $this->reflector,
            new Identifier(self::CLOSURE_NAME, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION))
        );

        /** @var Node\Stmt\Function_ $functionNode */
        $functionNode = $closureReflection->getNode();

        $this->node->stmts = $functionNode->stmts;
    }

    /**
     * Override the method or function's body of statements with an entirely new
     * body of statements within the reflection.
     *
     * @example
     * $reflectionFunction->setBodyFromString('return true;');
     */
    public function setBodyFromString(string $newBody) : void
    {
        $this->node->stmts = $this->loadStaticParser()->parse('<?php ' . $newBody);
    }

    /**
     * Override the method or function's body of statements with an entirely new
     * body of statements within the reflection.
     *
     * @param Node[] $nodes
     *
     * @example
     * // $ast should be an array of Nodes
     * $reflectionFunction->setBodyFromAst($ast);
     */
    public function setBodyFromAst(array $nodes) : void
    {
        // This slightly confusing code simply type-checks the $sourceLocators
        // array by unpacking them and splatting them in the closure.
        $validator         = static function (Node ...$node) : array {
            return $node;
        };
        $this->node->stmts = $validator(...$nodes);
    }

    /**
     * Add a new parameter to the method/function.
     */
    public function addParameter(string $parameterName) : void
    {
        $this->node->params[] = new ParamNode(new Node\Expr\Variable($parameterName));
    }

    /**
     * Remove a parameter from the method/function.
     */
    public function removeParameter(string $parameterName) : void
    {
        $lowerName = strtolower($parameterName);

        foreach ($this->node->params as $key => $paramNode) {
            if ($paramNode->var instanceof Node\Expr\Error) {
                throw new LogicException('PhpParser left an "Error" node in the parameters AST, this should NOT happen');
            }

            if (! is_string($paramNode->var->name) || strtolower($paramNode->var->name) !== $lowerName) {
                continue;
            }

            unset($this->node->params[$key]);
        }
    }

    /**
     * Fetch an array of all return statements found within this function.
     *
     * Note that return statements within smaller scopes contained (e.g. anonymous classes, closures) are not returned
     * here as they are not within the immediate scope.
     *
     * @return Node\Stmt\Return_[]
     */
    public function getReturnStatementsAst() : array
    {
        $visitor = new ReturnNodeVisitor();

        $traverser = new NodeTraverser();
        $traverser->addVisitor($visitor);

        $traverser->traverse($this->node->getStmts());

        return $visitor->getReturnNodes();
    }

    private function loadStaticParser() : Parser
    {
        return self::$parser ?? self::$parser = (new BetterReflection())->phpParser();
    }
}