* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Behat\Behat\Context\Reader; use Behat\Behat\Context\Annotation\AnnotationReader; use Behat\Behat\Context\Environment\ContextEnvironment; use Behat\Testwork\Call\Callee; use ReflectionClass; use ReflectionException; use ReflectionMethod; /** * Reads context callees by annotations using registered annotation readers. * * @author Konstantin Kudryashov */ final class AnnotatedContextReader implements ContextReader { const DOCLINE_TRIMMER_REGEX = '/^\/\*\*\s*|^\s*\*\s*|\s*\*\/$|\s*$/'; /** * @var string[] */ private static $ignoreAnnotations = array( '@param', '@return', '@throws', '@see', '@uses', '@todo' ); /** * @var AnnotationReader[] */ private $readers = array(); /** * Registers annotation reader. * * @param AnnotationReader $reader */ public function registerAnnotationReader(AnnotationReader $reader) { $this->readers[] = $reader; } /** * {@inheritdoc} */ public function readContextCallees(ContextEnvironment $environment, $contextClass) { $reflection = new ReflectionClass($contextClass); $callees = array(); foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { foreach ($this->readMethodCallees($reflection->getName(), $method) as $callee) { $callees[] = $callee; } } return $callees; } /** * Loads callees associated with specific method. * * @param string $class * @param ReflectionMethod $method * * @return Callee[] */ private function readMethodCallees($class, ReflectionMethod $method) { $callees = array(); // read parent annotations try { $prototype = $method->getPrototype(); // error occurs on every second PHP stable release - getPrototype() returns itself if ($prototype->getDeclaringClass()->getName() !== $method->getDeclaringClass()->getName()) { $callees = array_merge($callees, $this->readMethodCallees($class, $prototype)); } } catch (ReflectionException $e) { } if ($docBlock = $method->getDocComment()) { $callees = array_merge($callees, $this->readDocBlockCallees($class, $method, $docBlock)); } return $callees; } /** * Reads callees from the method doc block. * * @param string $class * @param ReflectionMethod $method * @param string $docBlock * * @return Callee[] */ private function readDocBlockCallees($class, ReflectionMethod $method, $docBlock) { $callees = array(); $description = $this->readDescription($docBlock); $docBlock = $this->mergeMultilines($docBlock); foreach (explode("\n", $docBlock) as $docLine) { $docLine = preg_replace(self::DOCLINE_TRIMMER_REGEX, '', $docLine); if ($this->isEmpty($docLine)) { continue; } if ($this->isNotAnnotation($docLine)) { continue; } if ($callee = $this->readDocLineCallee($class, $method, $docLine, $description)) { $callees[] = $callee; } } return $callees; } /** * Merges multiline strings (strings ending with "\") * * @param string $docBlock * * @return string */ private function mergeMultilines($docBlock) { return preg_replace("#\\\\$\s*+\*\s*+([^\\\\$]++)#m", '$1', $docBlock); } /** * Extracts a description from the provided docblock, * with support for multiline descriptions. * * @param string $docBlock * * @return string */ private function readDescription($docBlock) { // Remove indentation $description = preg_replace('/^[\s\t]*/m', '', $docBlock); // Remove block comment syntax $description = preg_replace('/^\/\*\*\s*|^\s*\*\s|^\s*\*\/$/m', '', $description); // Remove annotations $description = preg_replace('/^@.*$/m', '', $description); // Ignore docs after a "--" separator if (preg_match('/^--.*$/m', $description)) { $descriptionParts = preg_split('/^--.*$/m', $description); $description = array_shift($descriptionParts); } // Trim leading and trailing newlines $description = trim($description, "\r\n"); return $description; } /** * Checks if provided doc lien is empty. * * @param string $docLine * * @return Boolean */ private function isEmpty($docLine) { return '' == $docLine; } /** * Checks if provided doc line is not an annotation. * * @param string $docLine * * @return Boolean */ private function isNotAnnotation($docLine) { return '@' !== substr($docLine, 0, 1); } /** * Reads callee from provided doc line using registered annotation readers. * * @param string $class * @param ReflectionMethod $method * @param string $docLine * @param null|string $description * * @return null|Callee */ private function readDocLineCallee($class, ReflectionMethod $method, $docLine, $description = null) { if ($this->isIgnoredAnnotation($docLine)) { return null; } foreach ($this->readers as $reader) { if ($callee = $reader->readCallee($class, $method, $docLine, $description)) { return $callee; } } return null; } /** * Checks if provided doc line is one of the ignored annotations. * * @param string $docLine * * @return Boolean */ private function isIgnoredAnnotation($docLine) { $lowDocLine = strtolower($docLine); foreach (self::$ignoreAnnotations as $ignoredAnnotation) { if ($ignoredAnnotation == substr($lowDocLine, 0, strlen($ignoredAnnotation))) { return true; } } return false; } }