<?php 
 
namespace Doctrine\Common\Annotations; 
 
use Doctrine\Common\Annotations\Annotation\IgnoreAnnotation; 
use Doctrine\Common\Annotations\Annotation\Target; 
use ReflectionClass; 
use ReflectionFunction; 
use ReflectionMethod; 
use ReflectionProperty; 
 
use function array_merge; 
use function class_exists; 
use function extension_loaded; 
use function ini_get; 
 
/** 
 * A reader for docblock annotations. 
 */ 
class AnnotationReader implements Reader 
{ 
    /** 
     * Global map for imports. 
     * 
     * @var array<string, class-string> 
     */ 
    private static $globalImports = [ 
        'ignoreannotation' => Annotation\IgnoreAnnotation::class, 
    ]; 
 
    /** 
     * A list with annotations that are not causing exceptions when not resolved to an annotation class. 
     * 
     * The names are case sensitive. 
     * 
     * @var array<string, true> 
     */ 
    private static $globalIgnoredNames = ImplicitlyIgnoredAnnotationNames::LIST; 
 
    /** 
     * A list with annotations that are not causing exceptions when not resolved to an annotation class. 
     * 
     * The names are case sensitive. 
     * 
     * @var array<string, true> 
     */ 
    private static $globalIgnoredNamespaces = []; 
 
    /** 
     * Add a new annotation to the globally ignored annotation names with regard to exception handling. 
     * 
     * @param string $name 
     */ 
    public static function addGlobalIgnoredName($name) 
    { 
        self::$globalIgnoredNames[$name] = true; 
    } 
 
    /** 
     * Add a new annotation to the globally ignored annotation namespaces with regard to exception handling. 
     * 
     * @param string $namespace 
     */ 
    public static function addGlobalIgnoredNamespace($namespace) 
    { 
        self::$globalIgnoredNamespaces[$namespace] = true; 
    } 
 
    /** 
     * Annotations parser. 
     * 
     * @var DocParser 
     */ 
    private $parser; 
 
    /** 
     * Annotations parser used to collect parsing metadata. 
     * 
     * @var DocParser 
     */ 
    private $preParser; 
 
    /** 
     * PHP parser used to collect imports. 
     * 
     * @var PhpParser 
     */ 
    private $phpParser; 
 
    /** 
     * In-memory cache mechanism to store imported annotations per class. 
     * 
     * @psalm-var array<'class'|'function', array<string, array<string, class-string>>> 
     */ 
    private $imports = []; 
 
    /** 
     * In-memory cache mechanism to store ignored annotations per class. 
     * 
     * @psalm-var array<'class'|'function', array<string, array<string, true>>> 
     */ 
    private $ignoredAnnotationNames = []; 
 
    /** 
     * Initializes a new AnnotationReader. 
     * 
     * @throws AnnotationException 
     */ 
    public function __construct(?DocParser $parser = null) 
    { 
        if ( 
            extension_loaded('Zend Optimizer+') && (ini_get('zend_optimizerplus.save_comments') === '0' || 
            ini_get('opcache.save_comments') === '0') 
        ) { 
            throw AnnotationException::optimizerPlusSaveComments(); 
        } 
 
        if (extension_loaded('Zend OPcache') && ini_get('opcache.save_comments') === 0) { 
            throw AnnotationException::optimizerPlusSaveComments(); 
        } 
 
        // Make sure that the IgnoreAnnotation annotation is loaded 
        class_exists(IgnoreAnnotation::class); 
 
        $this->parser = $parser ?: new DocParser(); 
 
        $this->preParser = new DocParser(); 
 
        $this->preParser->setImports(self::$globalImports); 
        $this->preParser->setIgnoreNotImportedAnnotations(true); 
        $this->preParser->setIgnoredAnnotationNames(self::$globalIgnoredNames); 
 
        $this->phpParser = new PhpParser(); 
    } 
 
    /** 
     * {@inheritDoc} 
     */ 
    public function getClassAnnotations(ReflectionClass $class) 
    { 
        $this->parser->setTarget(Target::TARGET_CLASS); 
        $this->parser->setImports($this->getImports($class)); 
        $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 
        $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 
 
        return $this->parser->parse($class->getDocComment(), 'class ' . $class->getName()); 
    } 
 
    /** 
     * {@inheritDoc} 
     */ 
    public function getClassAnnotation(ReflectionClass $class, $annotationName) 
    { 
        $annotations = $this->getClassAnnotations($class); 
 
        foreach ($annotations as $annotation) { 
            if ($annotation instanceof $annotationName) { 
                return $annotation; 
            } 
        } 
 
        return null; 
    } 
 
    /** 
     * {@inheritDoc} 
     */ 
    public function getPropertyAnnotations(ReflectionProperty $property) 
    { 
        $class   = $property->getDeclaringClass(); 
        $context = 'property ' . $class->getName() . '::$' . $property->getName(); 
 
        $this->parser->setTarget(Target::TARGET_PROPERTY); 
        $this->parser->setImports($this->getPropertyImports($property)); 
        $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 
        $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 
 
        return $this->parser->parse($property->getDocComment(), $context); 
    } 
 
    /** 
     * {@inheritDoc} 
     */ 
    public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) 
    { 
        $annotations = $this->getPropertyAnnotations($property); 
 
        foreach ($annotations as $annotation) { 
            if ($annotation instanceof $annotationName) { 
                return $annotation; 
            } 
        } 
 
        return null; 
    } 
 
    /** 
     * {@inheritDoc} 
     */ 
    public function getMethodAnnotations(ReflectionMethod $method) 
    { 
        $class   = $method->getDeclaringClass(); 
        $context = 'method ' . $class->getName() . '::' . $method->getName() . '()'; 
 
        $this->parser->setTarget(Target::TARGET_METHOD); 
        $this->parser->setImports($this->getMethodImports($method)); 
        $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($class)); 
        $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 
 
        return $this->parser->parse($method->getDocComment(), $context); 
    } 
 
    /** 
     * {@inheritDoc} 
     */ 
    public function getMethodAnnotation(ReflectionMethod $method, $annotationName) 
    { 
        $annotations = $this->getMethodAnnotations($method); 
 
        foreach ($annotations as $annotation) { 
            if ($annotation instanceof $annotationName) { 
                return $annotation; 
            } 
        } 
 
        return null; 
    } 
 
    /** 
     * Gets the annotations applied to a function. 
     * 
     * @phpstan-return list<object> An array of Annotations. 
     */ 
    public function getFunctionAnnotations(ReflectionFunction $function): array 
    { 
        $context = 'function ' . $function->getName(); 
 
        $this->parser->setTarget(Target::TARGET_FUNCTION); 
        $this->parser->setImports($this->getImports($function)); 
        $this->parser->setIgnoredAnnotationNames($this->getIgnoredAnnotationNames($function)); 
        $this->parser->setIgnoredAnnotationNamespaces(self::$globalIgnoredNamespaces); 
 
        return $this->parser->parse($function->getDocComment(), $context); 
    } 
 
    /** 
     * Gets a function annotation. 
     * 
     * @return object|null The Annotation or NULL, if the requested annotation does not exist. 
     */ 
    public function getFunctionAnnotation(ReflectionFunction $function, string $annotationName) 
    { 
        $annotations = $this->getFunctionAnnotations($function); 
 
        foreach ($annotations as $annotation) { 
            if ($annotation instanceof $annotationName) { 
                return $annotation; 
            } 
        } 
 
        return null; 
    } 
 
    /** 
     * Returns the ignored annotations for the given class or function. 
     * 
     * @param ReflectionClass|ReflectionFunction $reflection 
     * 
     * @return array<string, true> 
     */ 
    private function getIgnoredAnnotationNames($reflection): array 
    { 
        $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; 
        $name = $reflection->getName(); 
 
        if (isset($this->ignoredAnnotationNames[$type][$name])) { 
            return $this->ignoredAnnotationNames[$type][$name]; 
        } 
 
        $this->collectParsingMetadata($reflection); 
 
        return $this->ignoredAnnotationNames[$type][$name]; 
    } 
 
    /** 
     * Retrieves imports for a class or a function. 
     * 
     * @param ReflectionClass|ReflectionFunction $reflection 
     * 
     * @return array<string, class-string> 
     */ 
    private function getImports($reflection): array 
    { 
        $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; 
        $name = $reflection->getName(); 
 
        if (isset($this->imports[$type][$name])) { 
            return $this->imports[$type][$name]; 
        } 
 
        $this->collectParsingMetadata($reflection); 
 
        return $this->imports[$type][$name]; 
    } 
 
    /** 
     * Retrieves imports for methods. 
     * 
     * @return array<string, class-string> 
     */ 
    private function getMethodImports(ReflectionMethod $method) 
    { 
        $class        = $method->getDeclaringClass(); 
        $classImports = $this->getImports($class); 
 
        $traitImports = []; 
 
        foreach ($class->getTraits() as $trait) { 
            if ( 
                ! $trait->hasMethod($method->getName()) 
                || $trait->getFileName() !== $method->getFileName() 
            ) { 
                continue; 
            } 
 
            $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait)); 
        } 
 
        return array_merge($classImports, $traitImports); 
    } 
 
    /** 
     * Retrieves imports for properties. 
     * 
     * @return array<string, class-string> 
     */ 
    private function getPropertyImports(ReflectionProperty $property) 
    { 
        $class        = $property->getDeclaringClass(); 
        $classImports = $this->getImports($class); 
 
        $traitImports = []; 
 
        foreach ($class->getTraits() as $trait) { 
            if (! $trait->hasProperty($property->getName())) { 
                continue; 
            } 
 
            $traitImports = array_merge($traitImports, $this->phpParser->parseUseStatements($trait)); 
        } 
 
        return array_merge($classImports, $traitImports); 
    } 
 
    /** 
     * Collects parsing metadata for a given class or function. 
     * 
     * @param ReflectionClass|ReflectionFunction $reflection 
     */ 
    private function collectParsingMetadata($reflection): void 
    { 
        $type = $reflection instanceof ReflectionClass ? 'class' : 'function'; 
        $name = $reflection->getName(); 
 
        $ignoredAnnotationNames = self::$globalIgnoredNames; 
        $annotations            = $this->preParser->parse($reflection->getDocComment(), $type . ' ' . $name); 
 
        foreach ($annotations as $annotation) { 
            if (! ($annotation instanceof IgnoreAnnotation)) { 
                continue; 
            } 
 
            foreach ($annotation->names as $annot) { 
                $ignoredAnnotationNames[$annot] = true; 
            } 
        } 
 
        $this->imports[$type][$name] = array_merge( 
            self::$globalImports, 
            $this->phpParser->parseUseStatements($reflection), 
            [ 
                '__NAMESPACE__' => $reflection->getNamespaceName(), 
                'self' => $name, 
            ] 
        ); 
 
        $this->ignoredAnnotationNames[$type][$name] = $ignoredAnnotationNames; 
    } 
}