Skip to content

Commit

Permalink
Avoiding (de-)serialization of reflection by extracting everything at…
Browse files Browse the repository at this point in the history
… construct
  • Loading branch information
Ocramius authored and sebastianbergmann committed Sep 7, 2019
1 parent c38b65c commit f57e88b
Show file tree
Hide file tree
Showing 2 changed files with 134 additions and 105 deletions.
177 changes: 118 additions & 59 deletions src/Annotation/DocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,44 +43,103 @@ final class DocBlock

private const REGEX_EXPECTED_EXCEPTION = '(@expectedException\s+([:.\w\\\\x7f-\xff]+)(?:[\t ]+(\S*))?(?:[\t ]+(\S*))?\s*$)m';

/** @var \ReflectionClass|\ReflectionFunctionAbstract */
private $reflector;
/** @var string */
private $docComment;

private function __construct()
{
/** @var bool */
private $isClass;

/** @var bool */
private $isMethod;

/** @var array<string, array<int, string>> */
private $symbolAnnotations;

/** @var int */
private $startLine;

/** @var int */
private $endLine;

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

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

/**
* Note: we do not preserve an instance of the reflection object, since it cannot be safely (de-)serialized.
*
* @param array<string, array<int, string>> $symbolAnnotations
*/
private function __construct(
string $docBlock,
bool $isClass,
bool $isMethod,
array $symbolAnnotations,
int $startLine,
int $endLine,
string $fileName,
string $name,
?string $className
) {
$this->docComment = $docBlock;
$this->isClass = $isClass;
$this->isMethod = $isMethod;
$this->symbolAnnotations = $symbolAnnotations;
$this->startLine = $startLine;
$this->endLine = $endLine;
$this->fileName = $fileName;
$this->name = $name;
$this->className = $className;
}

public static function ofClass(\ReflectionClass $class) : self
{
$instance = new self();

$instance->reflector = $class;

return $instance;
$className = $class->getName();

return new self(
(string) $class->getDocComment(),
true,
false,
self::extractAnnotationsFromReflector($class),
$class->getStartLine(),
$class->getEndLine(),
$class->getFileName(),
$className,
$className
);
}

public static function ofFunction(\ReflectionFunctionAbstract $function) : self
{
$instance = new self();

$instance->reflector = $function;

return $instance;
return new self(
(string) $function->getDocComment(),
false,
$function instanceof \ReflectionMethod,
self::extractAnnotationsFromReflector($function),
$function->getStartLine(),
$function->getEndLine(),
$function->getFileName(),
$function->getName(),
$function instanceof \ReflectionMethod
? $function->getDeclaringClass()->getName()
: null
);
}

// @TODO accurately document returned type here
public function requirements() : array
{
$docComment = (string) $this->reflector->getDocComment();
$offset = $this->reflector->getStartLine();
$offset = $this->startLine;
$requires = [
'__OFFSET' => [
'__FILE' => \realpath($this->reflector->getFileName()),
'__FILE' => \realpath($this->fileName),
],
];

// Split docblock into lines and rewind offset to start of docblock
$lines = \preg_split('/\r\n|\r|\n/', $docComment);
$lines = \preg_split('/\r\n|\r|\n/', $this->docComment);
$offset -= \count($lines);

foreach ($lines as $line) {
Expand Down Expand Up @@ -160,13 +219,13 @@ public function requirements() : array
*/
public function expectedException()
{
$docComment = (string) \substr((string) $this->reflector->getDocComment(), 3, -2);
$docComment = (string) \substr($this->docComment, 3, -2);

if (1 !== \preg_match(self::REGEX_EXPECTED_EXCEPTION, $docComment, $matches)) {
return false;
}

$annotations = $this->parseSymbolAnnotations();
$annotations = $this->symbolAnnotations();
$class = $matches[1];
$code = null;
$message = '';
Expand Down Expand Up @@ -209,9 +268,8 @@ public function expectedException()
*/
public function getProvidedData() : ?array
{
$docComment = (string) $this->reflector->getDocComment();
$data = $this->getDataFromDataProviderAnnotation($docComment)
?? $this->getDataFromTestWithAnnotation($docComment);
$data = $this->getDataFromDataProviderAnnotation($this->docComment)
?? $this->getDataFromTestWithAnnotation($this->docComment);

if ($data === null) {
return null;
Expand Down Expand Up @@ -240,10 +298,10 @@ public function getProvidedData() : ?array
*/
public function getInlineAnnotations() : array
{
$code = \file($this->reflector->getFileName());
$lineNumber = $this->reflector->getStartLine();
$startLine = $this->reflector->getStartLine() - 1;
$endLine = $this->reflector->getEndLine() - 1;
$code = \file($this->fileName);
$lineNumber = $this->startLine;
$startLine = $this->startLine - 1;
$endLine = $this->endLine - 1;
$codeLines = \array_slice($code, $startLine, $endLine - $startLine + 1);
$annotations = [];

Expand All @@ -261,49 +319,35 @@ public function getInlineAnnotations() : array
return $annotations;
}

public function parseSymbolAnnotations() : array
public function symbolAnnotations() : array
{
$annotations = [];

if ($this->reflector instanceof \ReflectionClass) {
$annotations = \array_merge(
$annotations,
...array_map(function (\ReflectionClass $trait) : array {
return $this->parseDocBlock((string) $trait->getDocComment());
}, \array_values($this->reflector->getTraits()))
);
}

return \array_merge(
$annotations,
$this->parseDocBlock((string) $this->reflector->getDocComment())
);
return $this->symbolAnnotations;
}

public function isHookToBeExecutedBeforeClass() : bool
{
return $this->reflector instanceof \ReflectionMethod
&& false !== \strpos((string) $this->reflector->getDocComment(), '@beforeClass');
return $this->isMethod
&& false !== \strpos($this->docComment, '@beforeClass');
}

public function isHookToBeExecutedAfterClass() : bool
{
return $this->reflector instanceof \ReflectionMethod
&& false !== \strpos((string) $this->reflector->getDocComment(), '@afterClass');
return $this->isMethod
&& false !== \strpos($this->docComment, '@afterClass');
}

public function isToBeExecutedBeforeTest() : bool
{
return 1 === \preg_match('/@before\b/', (string) $this->reflector->getDocComment());
return 1 === \preg_match('/@before\b/', $this->docComment);
}

public function isToBeExecutedAfterTest() : bool
{
return 1 === \preg_match('/@after\b/', (string) $this->reflector->getDocComment());
return 1 === \preg_match('/@after\b/', $this->docComment);
}

/** @return array<string, array<int, string>> */
private function parseDocBlock(string $docBlock) : array
private static function parseDocBlock(string $docBlock) : array
{
// Strip away the docblock header and footer to ease parsing of one line annotations
$docBlock = (string) \substr($docBlock, 3, -2);
Expand Down Expand Up @@ -344,16 +388,11 @@ private function parseAnnotationContent(string $message) : string
private function getDataFromDataProviderAnnotation(string $docComment): ?iterable
{
$methodName = null;
$className = null;
$className = $this->className;

if ($this->reflector instanceof \ReflectionMethod) {
$methodName = $this->reflector->getName();
$className = $this->reflector->getDeclaringClass()
->getName();
}

if ($this->reflector instanceof \ReflectionClass) {
$className = $this->reflector->getName();
if ($this->isMethod) {
$methodName = $this->name;
$className = $this->className;
}

if (! \preg_match_all(self::REGEX_DATA_PROVIDER, $docComment, $matches)) {
Expand Down Expand Up @@ -483,4 +522,24 @@ private function cleanUpMultiLineAnnotation(string $docComment): string

return \rtrim($docComment, "\n");
}

/** @param \ReflectionClass|\ReflectionFunctionAbstract $reflector */
private static function extractAnnotationsFromReflector(\Reflector $reflector) : array
{
$annotations = [];

if ($reflector instanceof \ReflectionClass) {
$annotations = \array_merge(
$annotations,
...array_map(function (\ReflectionClass $trait) : array {
return self::parseDocBlock((string) $trait->getDocComment());
}, \array_values($reflector->getTraits()))
);
}

return \array_merge(
$annotations,
self::parseDocBlock((string) $reflector->getDocComment())
);
}
}
62 changes: 16 additions & 46 deletions src/Util/Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,6 @@ final class Test
*/
public const REGEX_DATA_PROVIDER = '/@dataProvider\s+([a-zA-Z0-9._:-\\\\x7f-\xff]+)/';

/**
* @var array
*/
private static $annotationCache = [];

/**
* @var array
*/
Expand Down Expand Up @@ -309,27 +304,28 @@ public static function getProvidedData(string $className, string $methodName): ?
->getProvidedData();
}

/**
* @deprecated please use the {@see \PHPUnit\Annotation\Docblock} API instead
*/
public static function parseTestMethodAnnotations(string $className, ?string $methodName = ''): array
{
if ($methodName === null) {
return [
'class' => Registry::singleton()
->forClassName($className)
->parseSymbolAnnotations(),
'method' => null,
];
if ($methodName !== null) {
try {
return [
'class' => Registry::singleton()
->forClassName($className)
->symbolAnnotations(),
'method' => Registry::singleton()
->forMethod($className, $methodName)
->symbolAnnotations(),
];
} catch (Exception $methodNotFound) {
// ignored
}
}

return [
'class' => Registry::singleton()
->forClassName($className)
->parseSymbolAnnotations(),
'method' => Registry::singleton()
->forMethod($className, $methodName)
->parseSymbolAnnotations(),
->symbolAnnotations(),
'method' => null,
];
}

Expand Down Expand Up @@ -534,7 +530,7 @@ public static function isTestMethod(\ReflectionMethod $method): bool
->getName(),
$method->getName()
)
->parseSymbolAnnotations()
->symbolAnnotations()
);
}

Expand Down Expand Up @@ -599,32 +595,6 @@ private static function getLinesToBeCoveredOrUsed(string $className, string $met
return self::resolveReflectionObjectsToLines($codeList);
}

/**
* Parse annotation content to use constant/class constant values
*
* Constants are specified using a starting '@'. For example: @ClassName::CONST_NAME
*
* If the constant is not found the string is used as is to ensure maximum BC.
*/
private static function parseAnnotationContent(string $message): string
{
if (\defined($message) && (\strpos($message, '::') !== false && \substr_count($message, '::') + 1 === 2)) {
$message = \constant($message);
}

return $message;
}

private static function cleanUpMultiLineAnnotation(string $docComment): string
{
//removing initial ' * ' for docComment
$docComment = \str_replace("\r\n", "\n", $docComment);
$docComment = \preg_replace('/' . '\n' . '\s*' . '\*' . '\s?' . '/', "\n", $docComment);
$docComment = (string) \substr($docComment, 0, -1);

return \rtrim($docComment, "\n");
}

private static function emptyHookMethodsArray(): array
{
return [
Expand Down

0 comments on commit f57e88b

Please sign in to comment.