Skip to content

Commit

Permalink
Backport annotations from nette/reflection
Browse files Browse the repository at this point in the history
  • Loading branch information
f3l1x committed Dec 26, 2022
1 parent 57fc800 commit d9dffc0
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 0 deletions.
178 changes: 178 additions & 0 deletions src/Annotations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php declare(strict_types = 1);

namespace Contributte\Utils;

use Contributte\Utils\Exception\LogicalException;
use Nette\StaticClass;
use Nette\Utils\ArrayHash;
use Nette\Utils\Strings;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
use ReflectionProperty;
use Reflector;

/**
* @creator David Grudl https://github.com/nette/reflection
*/
class Annotations
{

use StaticClass;

/** @internal single & double quoted PHP string */
public const RE_STRING = '\'(?:\\\\.|[^\'\\\\])*\'|"(?:\\\\.|[^"\\\\])*"';

/** @internal identifier */
public const RE_IDENTIFIER = '[_a-zA-Z\x7F-\xFF][_a-zA-Z0-9\x7F-\xFF-\\\]*';

/** @var bool */
public static $useReflection;

/** @var bool */
public static $autoRefresh = true;

/** @var string[] */
public static $inherited = ['description', 'param', 'return'];

/** @var array<string, array<string, mixed[]>> */
private static $cache;

/**
* @param ReflectionClass<object>|ReflectionMethod|ReflectionProperty|ReflectionFunction $r
* @return array<mixed>
*/
public static function getAll(Reflector $r): array
{
if ($r instanceof ReflectionClass) {
$type = $r->getName();
$member = 'class';

} elseif ($r instanceof ReflectionMethod) {
$type = $r->getDeclaringClass()->getName();
$member = $r->getName();

} elseif ($r instanceof ReflectionFunction) {
$type = null;
$member = $r->getName();

} else {
$type = $r->getDeclaringClass()->getName();
$member = '$' . $r->getName();
}

if (self::$useReflection === null) { // detects whether is reflection available
self::$useReflection = (bool) (new ReflectionClass(self::class))->getDocComment();
}

if (isset(self::$cache[$type][$member])) { // is value cached?
return self::$cache[$type][$member];
}

if (self::$useReflection) {
$annotations = self::parseComment((string) $r->getDocComment());
} else {
$annotations = [];
}

// @phpstan-ignore-next-line
if ($r instanceof ReflectionMethod && !$r->isPrivate() && (!$r->isConstructor() || !empty($annotations['inheritdoc'][0]))
) {
try {
$inherited = self::getAll(new ReflectionMethod((string) get_parent_class($type), $member));
} catch (ReflectionException $e) {
try {
$inherited = self::getAll($r->getPrototype());
} catch (ReflectionException $e) {
$inherited = [];
}
}

$annotations += array_intersect_key($inherited, array_flip(self::$inherited));
}

return self::$cache[$type][$member] = $annotations;
}

/**
* @return array<mixed>
*/
private static function parseComment(string $comment): array
{
static $tokens = ['true' => true, 'false' => false, 'null' => null, '' => true];

$res = [];
$comment = (string) preg_replace('#^\s*\*\s?#ms', '', trim($comment, '/*'));
$parts = preg_split('#^\s*(?=@' . self::RE_IDENTIFIER . ')#m', $comment, 2);

if ($parts === false) {
throw new LogicalException('Cannot split comment');
}

$description = trim($parts[0]);
if ($description !== '') {
$res['description'] = [$description];
}

$matches = Strings::matchAll(
$parts[1] ?? '',
'~
(?<=\s|^)@(' . self::RE_IDENTIFIER . ')[ \t]* ## annotation
(
\((?>' . self::RE_STRING . '|[^\'")@]+)+\)| ## (value)
[^(@\r\n][^@\r\n]*|) ## value
~xi'
);

foreach ($matches as $match) {
[, $name, $value] = $match;

if (substr($value, 0, 1) === '(') {
$items = [];
$key = '';
$val = true;
$value[0] = ',';
while ($m = Strings::match($value, '#\s*,\s*(?>(' . self::RE_IDENTIFIER . ')\s*=\s*)?(' . self::RE_STRING . '|[^\'"),\s][^\'"),]*)#A')) {
$value = substr($value, strlen($m[0]));
[, $key, $val] = $m;
$val = rtrim($val);
if ($val[0] === "'" || $val[0] === '"') {
$val = substr($val, 1, -1);

} elseif (is_numeric($val)) {
$val = 1 * $val;

} else {
$lval = strtolower($val);
$val = array_key_exists($lval, $tokens) ? $tokens[$lval] : $val;
}

if ($key === '') {
$items[] = $val;

} else {
$items[$key] = $val;
}
}

$value = count($items) < 2 && $key === '' ? $val : $items;

} else {
$value = trim($value);
if (is_numeric($value)) {
$value = 1 * $value;

} else {
$lval = strtolower($value);
$value = array_key_exists($lval, $tokens) ? $tokens[$lval] : $value;
}
}

$res[$name][] = is_array($value) ? ArrayHash::from($value) : $value;
}

return $res;
}

}
10 changes: 10 additions & 0 deletions src/Exception/LogicalException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);

namespace Contributte\Utils\Exception;

use LogicException;

class LogicalException extends LogicException
{

}
25 changes: 25 additions & 0 deletions tests/Cases/Annotations.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);

require_once __DIR__ . '/../bootstrap.php';

use Contributte\Utils\Annotations;
use Tester\Assert;
use Tests\Fixtures\AnnotationFoo;

// Class
test(function (): void {
$annotations = Annotations::getAll(new ReflectionClass(AnnotationFoo::class));

Assert::count(2, $annotations);
Assert::equal(['DG'], $annotations['creator']);
Assert::equal([true], $annotations['test']);
});

// Method
test(function (): void {
$annotations = Annotations::getAll(new ReflectionMethod(AnnotationFoo::class, 'fake'));

Assert::count(2, $annotations);
Assert::equal(['Felix'], $annotations['creator']);
Assert::equal([false], $annotations['test']);
});
21 changes: 21 additions & 0 deletions tests/Fixtures/AnnotationFoo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);

namespace Tests\Fixtures;

/**
* @creator DG
* @test(true)
*/
class AnnotationFoo
{

/**
* @creator Felix
* @test(false)
*/
public function fake(): bool
{
return true;
}

}

0 comments on commit d9dffc0

Please sign in to comment.