welcome back to dyb-tech
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
interface AnalyserInterface
|
||||
{
|
||||
public function setGenerator(Generator $generator): void;
|
||||
|
||||
public function fromFile(string $filename, Context $context): Analysis;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
interface AnnotationFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Checks if this factory is supported by the current runtime.
|
||||
*/
|
||||
public function isSupported(): bool;
|
||||
|
||||
public function setGenerator(Generator $generator): void;
|
||||
|
||||
/**
|
||||
* @return array<OA\AbstractAnnotation> top level annotations
|
||||
*/
|
||||
public function build(\Reflector $reflector, Context $context): array;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
class AttributeAnnotationFactory implements AnnotationFactoryInterface
|
||||
{
|
||||
/** @var Generator|null */
|
||||
protected $generator;
|
||||
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return \PHP_VERSION_ID >= 80100;
|
||||
}
|
||||
|
||||
public function setGenerator(Generator $generator): void
|
||||
{
|
||||
$this->generator = $generator;
|
||||
}
|
||||
|
||||
public function build(\Reflector $reflector, Context $context): array
|
||||
{
|
||||
if (!$this->isSupported() || !method_exists($reflector, 'getAttributes')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($reflector instanceof \ReflectionProperty && method_exists($reflector, 'isPromoted') && $reflector->isPromoted()) {
|
||||
// handled via __construct() parameter
|
||||
return [];
|
||||
}
|
||||
|
||||
// no proper way to inject
|
||||
Generator::$context = $context;
|
||||
|
||||
/** @var OA\AbstractAnnotation[] $annotations */
|
||||
$annotations = [];
|
||||
try {
|
||||
foreach ($reflector->getAttributes() as $attribute) {
|
||||
if (class_exists($attribute->getName())) {
|
||||
$instance = $attribute->newInstance();
|
||||
if ($instance instanceof OA\AbstractAnnotation) {
|
||||
$annotations[] = $instance;
|
||||
}
|
||||
} else {
|
||||
$context->logger->debug(sprintf('Could not instantiate attribute "%s", because class not found.', $attribute->getName()));
|
||||
}
|
||||
}
|
||||
|
||||
if ($reflector instanceof \ReflectionMethod) {
|
||||
// also look at parameter attributes
|
||||
foreach ($reflector->getParameters() as $rp) {
|
||||
foreach ([OA\Property::class, OA\Parameter::class, OA\RequestBody::class] as $attributeName) {
|
||||
foreach ($rp->getAttributes($attributeName, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
|
||||
/** @var OA\Property|OA\Parameter|OA\RequestBody $instance */
|
||||
$instance = $attribute->newInstance();
|
||||
$type = (($rnt = $rp->getType()) && $rnt instanceof \ReflectionNamedType) ? $rnt->getName() : Generator::UNDEFINED;
|
||||
$nullable = $rnt ? $rnt->allowsNull() : true;
|
||||
|
||||
if ($instance instanceof OA\RequestBody) {
|
||||
$instance->required = !$nullable;
|
||||
} elseif ($instance instanceof OA\Property) {
|
||||
if (Generator::isDefault($instance->property)) {
|
||||
$instance->property = $rp->getName();
|
||||
}
|
||||
if (Generator::isDefault($instance->type)) {
|
||||
$instance->type = $type;
|
||||
}
|
||||
$instance->nullable = $nullable ?: Generator::UNDEFINED;
|
||||
|
||||
if ($rp->isPromoted()) {
|
||||
// promoted parameter - docblock is available via class/property
|
||||
if ($comment = $rp->getDeclaringClass()->getProperty($rp->getName())->getDocComment()) {
|
||||
$instance->_context->comment = $comment;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$instance->name || Generator::isDefault($instance->name)) {
|
||||
$instance->name = $rp->getName();
|
||||
}
|
||||
$instance->required = !$nullable;
|
||||
$context = new Context(['nested' => $this], $context);
|
||||
$context->comment = null;
|
||||
$instance->merge([new OA\Schema(['type' => $type, '_context' => $context])]);
|
||||
}
|
||||
$annotations[] = $instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (($rrt = $reflector->getReturnType()) && $rrt instanceof \ReflectionNamedType) {
|
||||
foreach ($annotations as $annotation) {
|
||||
if ($annotation instanceof OA\Property && Generator::isDefault($annotation->type)) {
|
||||
// pick up simple return types
|
||||
$annotation->type = $rrt->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Generator::$context = null;
|
||||
}
|
||||
|
||||
$annotations = array_values(array_filter($annotations, function ($a) {
|
||||
return $a instanceof OA\AbstractAnnotation;
|
||||
}));
|
||||
|
||||
// merge backwards into parents...
|
||||
$isParent = function (OA\AbstractAnnotation $annotation, OA\AbstractAnnotation $possibleParent): bool {
|
||||
// regular annotation hierarchy
|
||||
$explicitParent = null !== $possibleParent->matchNested($annotation) && !$annotation instanceof OA\Attachable;
|
||||
|
||||
$isParentAllowed = false;
|
||||
// support Attachable subclasses
|
||||
if ($isAttachable = $annotation instanceof OA\Attachable) {
|
||||
if (!$isParentAllowed = (null === $annotation->allowedParents())) {
|
||||
// check for allowed parents
|
||||
foreach ($annotation->allowedParents() as $allowedParent) {
|
||||
if ($possibleParent instanceof $allowedParent) {
|
||||
$isParentAllowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Property can be nested...
|
||||
return $annotation->getRoot() != $possibleParent->getRoot()
|
||||
&& ($explicitParent || ($isAttachable && $isParentAllowed));
|
||||
};
|
||||
|
||||
$annotationsWithoutParent = [];
|
||||
foreach ($annotations as $index => $annotation) {
|
||||
$mergedIntoParent = false;
|
||||
|
||||
for ($ii = 0; $ii < count($annotations); ++$ii) {
|
||||
if ($ii === $index) {
|
||||
continue;
|
||||
}
|
||||
$possibleParent = $annotations[$ii];
|
||||
if ($isParent($annotation, $possibleParent)) {
|
||||
$mergedIntoParent = true; //
|
||||
$possibleParent->merge([$annotation]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$mergedIntoParent) {
|
||||
$annotationsWithoutParent[] = $annotation;
|
||||
}
|
||||
}
|
||||
|
||||
return $annotationsWithoutParent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use Composer\Autoload\ClassLoader;
|
||||
|
||||
/**
|
||||
* Scans for classes/interfaces/traits.
|
||||
*
|
||||
* Relies on a `composer --optimized` run in order to utilize
|
||||
* the generated class map.
|
||||
*/
|
||||
class ComposerAutoloaderScanner
|
||||
{
|
||||
/**
|
||||
* Collect all classes/interfaces/traits known by composer.
|
||||
*
|
||||
* @param array<string> $namespaces
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function scan(array $namespaces): array
|
||||
{
|
||||
$units = [];
|
||||
if ($autoloader = $this->getComposerAutoloader()) {
|
||||
foreach (array_keys($autoloader->getClassMap()) as $unit) {
|
||||
foreach ($namespaces as $namespace) {
|
||||
if (0 === strpos($unit, $namespace)) {
|
||||
$units[] = $unit;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $units;
|
||||
}
|
||||
|
||||
public static function getComposerAutoloader(): ?ClassLoader
|
||||
{
|
||||
foreach (spl_autoload_functions() as $fkt) {
|
||||
if (is_array($fkt) && $fkt[0] instanceof ClassLoader) {
|
||||
return $fkt[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
class DocBlockAnnotationFactory implements AnnotationFactoryInterface
|
||||
{
|
||||
/** @var DocBlockParser|null */
|
||||
protected $docBlockParser = null;
|
||||
|
||||
/** @var Generator|null */
|
||||
protected $generator = null;
|
||||
|
||||
public function __construct(?DocBlockParser $docBlockParser = null)
|
||||
{
|
||||
$this->docBlockParser = $docBlockParser ?: new DocBlockParser();
|
||||
}
|
||||
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return DocBlockParser::isEnabled();
|
||||
}
|
||||
|
||||
public function setGenerator(Generator $generator): void
|
||||
{
|
||||
$this->generator = $generator;
|
||||
|
||||
$this->docBlockParser->setAliases($generator->getAliases());
|
||||
}
|
||||
|
||||
public function build(\Reflector $reflector, Context $context): array
|
||||
{
|
||||
$aliases = $this->generator ? $this->generator->getAliases() : [];
|
||||
|
||||
if (method_exists($reflector, 'getShortName') && method_exists($reflector, 'getName')) {
|
||||
$aliases[strtolower($reflector->getShortName())] = $reflector->getName();
|
||||
}
|
||||
|
||||
if ($context->with('scanned')) {
|
||||
$details = $context->scanned;
|
||||
foreach ($details['uses'] as $alias => $name) {
|
||||
$aliasKey = strtolower($alias);
|
||||
if ($name != $alias && !array_key_exists($aliasKey, $aliases)) {
|
||||
// real aliases only
|
||||
$aliases[strtolower($alias)] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->docBlockParser->setAliases($aliases);
|
||||
|
||||
if (method_exists($reflector, 'getDocComment') && ($comment = $reflector->getDocComment())) {
|
||||
return $this->docBlockParser->fromComment($comment, $context);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use Doctrine\Common\Annotations\DocParser;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
/**
|
||||
* Extract swagger-php annotations from a [PHPDoc](http://en.wikipedia.org/wiki/PHPDoc) using Doctrine's DocParser.
|
||||
*/
|
||||
class DocBlockParser
|
||||
{
|
||||
/**
|
||||
* @var DocParser
|
||||
*/
|
||||
protected $docParser;
|
||||
|
||||
/**
|
||||
* @param array<string, class-string> $aliases
|
||||
*/
|
||||
public function __construct(array $aliases = [])
|
||||
{
|
||||
if (DocBlockParser::isEnabled()) {
|
||||
$docParser = new DocParser();
|
||||
$docParser->setIgnoreNotImportedAnnotations(true);
|
||||
$docParser->setImports($aliases);
|
||||
$this->docParser = $docParser;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we can process annotations.
|
||||
*/
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
return class_exists('Doctrine\\Common\\Annotations\\DocParser');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, class-string> $aliases
|
||||
*/
|
||||
public function setAliases(array $aliases): void
|
||||
{
|
||||
$this->docParser->setImports($aliases);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use doctrine to parse the comment block and return the detected annotations.
|
||||
*
|
||||
* @param string $comment a T_DOC_COMMENT
|
||||
* @param Context $context
|
||||
*
|
||||
* @return array<OA\AbstractAnnotation>
|
||||
*/
|
||||
public function fromComment(string $comment, Context $context): array
|
||||
{
|
||||
$context->comment = $comment;
|
||||
|
||||
try {
|
||||
Generator::$context = $context;
|
||||
if ($context->is('annotations') === false) {
|
||||
$context->annotations = [];
|
||||
}
|
||||
|
||||
return $this->docParser->parse($comment, $context->getDebugLocation());
|
||||
} catch (\Exception $e) {
|
||||
if (preg_match('/^(.+) at position ([0-9]+) in ' . preg_quote((string) $context, '/') . '\.$/', $e->getMessage(), $matches)) {
|
||||
$errorMessage = $matches[1];
|
||||
$errorPos = (int) $matches[2];
|
||||
$atPos = strpos($comment, '@');
|
||||
$context->line -= substr_count($comment, "\n", $atPos + $errorPos) + 1;
|
||||
$lines = explode("\n", substr($comment, $atPos, $errorPos));
|
||||
$context->character = strlen(array_pop($lines)) + 1; // position starts at 0 character starts at 1
|
||||
$context->logger->error($errorMessage . ' in ' . $context, ['exception' => $e]);
|
||||
} else {
|
||||
$context->logger->error(
|
||||
$e->getMessage() . ($context->filename ? ('; file=' . $context->filename) : ''),
|
||||
['exception' => $e]
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
} finally {
|
||||
Generator::$context = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Annotations as OA;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
/**
|
||||
* OpenApi analyser using reflection.
|
||||
*
|
||||
* Can read either PHP `DocBlock`s or `Attribute`s.
|
||||
*
|
||||
* Due to the nature of reflection this requires all related classes
|
||||
* to be auto-loadable.
|
||||
*/
|
||||
class ReflectionAnalyser implements AnalyserInterface
|
||||
{
|
||||
/** @var AnnotationFactoryInterface[] */
|
||||
protected $annotationFactories;
|
||||
|
||||
/** @var Generator|null */
|
||||
protected $generator;
|
||||
|
||||
/**
|
||||
* @param array<AnnotationFactoryInterface> $annotationFactories
|
||||
*/
|
||||
public function __construct(array $annotationFactories = [])
|
||||
{
|
||||
$this->annotationFactories = [];
|
||||
foreach ($annotationFactories as $annotationFactory) {
|
||||
if ($annotationFactory->isSupported()) {
|
||||
$this->annotationFactories[] = $annotationFactory;
|
||||
}
|
||||
}
|
||||
if (!$this->annotationFactories) {
|
||||
throw new \RuntimeException('No suitable annotation factory found. At least one of "Doctrine Annotations" or PHP 8.1 are required');
|
||||
}
|
||||
}
|
||||
|
||||
public function setGenerator(Generator $generator): void
|
||||
{
|
||||
$this->generator = $generator;
|
||||
|
||||
foreach ($this->annotationFactories as $annotationFactory) {
|
||||
$annotationFactory->setGenerator($generator);
|
||||
}
|
||||
}
|
||||
|
||||
public function fromFile(string $filename, Context $context): Analysis
|
||||
{
|
||||
$scanner = new TokenScanner();
|
||||
$fileDetails = $scanner->scanFile($filename);
|
||||
|
||||
$analysis = new Analysis([], $context);
|
||||
foreach ($fileDetails as $fqdn => $details) {
|
||||
$this->analyzeFqdn($fqdn, $analysis, $details);
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
public function fromFqdn(string $fqdn, Analysis $analysis): Analysis
|
||||
{
|
||||
$fqdn = ltrim($fqdn, '\\');
|
||||
|
||||
$rc = new \ReflectionClass($fqdn);
|
||||
if (!$filename = $rc->getFileName()) {
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
$scanner = new TokenScanner();
|
||||
$fileDetails = $scanner->scanFile($filename);
|
||||
|
||||
$this->analyzeFqdn($fqdn, $analysis, $fileDetails[$fqdn]);
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
protected function analyzeFqdn(string $fqdn, Analysis $analysis, array $details): Analysis
|
||||
{
|
||||
if (!class_exists($fqdn) && !interface_exists($fqdn) && !trait_exists($fqdn) && (!function_exists('enum_exists') || !enum_exists($fqdn))) {
|
||||
$analysis->context->logger->warning('Skipping unknown ' . $fqdn);
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
$rc = new \ReflectionClass($fqdn);
|
||||
$contextType = $rc->isInterface() ? 'interface' : ($rc->isTrait() ? 'trait' : ((method_exists($rc, 'isEnum') && $rc->isEnum()) ? 'enum' : 'class'));
|
||||
$context = new Context([
|
||||
$contextType => $rc->getShortName(),
|
||||
'namespace' => $rc->getNamespaceName() ?: null,
|
||||
'uses' => $details['uses'],
|
||||
'comment' => $rc->getDocComment() ?: null,
|
||||
'filename' => $rc->getFileName() ?: null,
|
||||
'line' => $rc->getStartLine(),
|
||||
'annotations' => [],
|
||||
'scanned' => $details,
|
||||
], $analysis->context);
|
||||
|
||||
$definition = [
|
||||
$contextType => $rc->getShortName(),
|
||||
'extends' => null,
|
||||
'implements' => [],
|
||||
'traits' => [],
|
||||
'properties' => [],
|
||||
'methods' => [],
|
||||
'context' => $context,
|
||||
];
|
||||
$normaliseClass = function (string $name): string {
|
||||
return '\\' . ltrim($name, '\\');
|
||||
};
|
||||
if ($parentClass = $rc->getParentClass()) {
|
||||
$definition['extends'] = $normaliseClass($parentClass->getName());
|
||||
}
|
||||
$definition[$contextType == 'class' ? 'implements' : 'extends'] = array_map($normaliseClass, $details['interfaces']);
|
||||
$definition['traits'] = array_map($normaliseClass, $details['traits']);
|
||||
|
||||
foreach ($this->annotationFactories as $annotationFactory) {
|
||||
$analysis->addAnnotations($annotationFactory->build($rc, $context), $context);
|
||||
}
|
||||
|
||||
foreach ($rc->getMethods() as $method) {
|
||||
if (in_array($method->name, $details['methods'])) {
|
||||
$definition['methods'][$method->getName()] = $ctx = new Context([
|
||||
'method' => $method->getName(),
|
||||
'comment' => $method->getDocComment() ?: null,
|
||||
'filename' => $method->getFileName() ?: null,
|
||||
'line' => $method->getStartLine(),
|
||||
'annotations' => [],
|
||||
], $context);
|
||||
foreach ($this->annotationFactories as $annotationFactory) {
|
||||
$analysis->addAnnotations($annotationFactory->build($method, $ctx), $ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rc->getProperties() as $property) {
|
||||
if (in_array($property->name, $details['properties'])) {
|
||||
$definition['properties'][$property->getName()] = $ctx = new Context([
|
||||
'property' => $property->getName(),
|
||||
'comment' => $property->getDocComment() ?: null,
|
||||
'annotations' => [],
|
||||
], $context);
|
||||
if ($property->isStatic()) {
|
||||
$ctx->static = true;
|
||||
}
|
||||
if (\PHP_VERSION_ID >= 70400 && ($type = $property->getType())) {
|
||||
$ctx->nullable = $type->allowsNull();
|
||||
if ($type instanceof \ReflectionNamedType) {
|
||||
$ctx->type = $type->getName();
|
||||
// Context::fullyQualifiedName(...) expects this
|
||||
if (class_exists($absFqn = '\\' . $ctx->type)) {
|
||||
$ctx->type = $absFqn;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($this->annotationFactories as $annotationFactory) {
|
||||
$analysis->addAnnotations($annotationFactory->build($property, $ctx), $ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rc->getReflectionConstants() as $constant) {
|
||||
foreach ($this->annotationFactories as $annotationFactory) {
|
||||
$definition['constants'][$constant->getName()] = $ctx = new Context([
|
||||
'constant' => $constant->getName(),
|
||||
'comment' => $constant->getDocComment() ?: null,
|
||||
'annotations' => [],
|
||||
], $context);
|
||||
foreach ($annotationFactory->build($constant, $ctx) as $annotation) {
|
||||
if ($annotation instanceof OA\Property) {
|
||||
if (Generator::isDefault($annotation->property)) {
|
||||
$annotation->property = $constant->getName();
|
||||
}
|
||||
if (Generator::isDefault($annotation->const)) {
|
||||
$annotation->const = $constant->getValue();
|
||||
}
|
||||
$analysis->addAnnotation($annotation, $ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$addDefinition = 'add' . ucfirst($contextType) . 'Definition';
|
||||
$analysis->{$addDefinition}($definition);
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,641 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
use OpenApi\Analysis;
|
||||
use OpenApi\Context;
|
||||
use OpenApi\Generator;
|
||||
|
||||
/**
|
||||
* Extracts swagger-php annotations from php code using static analysis.
|
||||
*/
|
||||
class TokenAnalyser implements AnalyserInterface
|
||||
{
|
||||
/** @var Generator|null */
|
||||
protected $generator;
|
||||
|
||||
public function setGenerator(Generator $generator): void
|
||||
{
|
||||
$this->generator = $generator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and process all doc-comments from a file.
|
||||
*
|
||||
* @param string $filename path to a php file
|
||||
*/
|
||||
public function fromFile(string $filename, Context $context): Analysis
|
||||
{
|
||||
if (function_exists('opcache_get_status') && function_exists('opcache_get_configuration')) {
|
||||
if (empty($GLOBALS['openapi_opcache_warning'])) {
|
||||
$GLOBALS['openapi_opcache_warning'] = true;
|
||||
$status = opcache_get_status();
|
||||
$config = opcache_get_configuration();
|
||||
if (is_array($status) && $status['opcache_enabled'] && $config['directives']['opcache.save_comments'] == false) {
|
||||
$context->logger->error("php.ini \"opcache.save_comments = 0\" interferes with extracting annotations.\n[LINK] https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.save-comments");
|
||||
}
|
||||
}
|
||||
}
|
||||
$tokens = token_get_all(file_get_contents($filename));
|
||||
|
||||
return $this->fromTokens($tokens, new Context(['filename' => $filename], $context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract and process all doc-comments from the contents.
|
||||
*
|
||||
* @param string $code PHP code. (including <?php tags)
|
||||
* @param Context $context the original location of the contents
|
||||
*/
|
||||
public function fromCode(string $code, Context $context): Analysis
|
||||
{
|
||||
$tokens = token_get_all($code);
|
||||
|
||||
return $this->fromTokens($tokens, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared implementation for parseFile() & parseContents().
|
||||
*
|
||||
* @param array $tokens The result of a token_get_all()
|
||||
*/
|
||||
protected function fromTokens(array $tokens, Context $parseContext): Analysis
|
||||
{
|
||||
$generator = $this->generator ?: new Generator();
|
||||
$analysis = new Analysis([], $parseContext);
|
||||
$docBlockParser = new DocBlockParser($generator->getAliases());
|
||||
|
||||
reset($tokens);
|
||||
$token = '';
|
||||
|
||||
$aliases = $generator->getAliases();
|
||||
|
||||
$parseContext->uses = [];
|
||||
// default to parse context to start with
|
||||
$schemaContext = $parseContext;
|
||||
|
||||
$classDefinition = false;
|
||||
$interfaceDefinition = false;
|
||||
$traitDefinition = false;
|
||||
$enumDefinition = false;
|
||||
$comment = false;
|
||||
|
||||
$line = 0;
|
||||
$lineOffset = $parseContext->line ?: 0;
|
||||
|
||||
while ($token !== false) {
|
||||
$previousToken = $token;
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
|
||||
if (is_array($token) === false) {
|
||||
// Ignore tokens like "{", "}", etc
|
||||
continue;
|
||||
}
|
||||
|
||||
if (defined('T_ATTRIBUTE') && $token[0] === T_ATTRIBUTE) {
|
||||
// consume
|
||||
$this->parseAttribute($tokens, $token, $parseContext);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($token[0] === T_DOC_COMMENT) {
|
||||
if ($comment) {
|
||||
// 2 Doc-comments in succession?
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, new Context(['line' => $line], $schemaContext));
|
||||
}
|
||||
$comment = $token[1];
|
||||
$line = $token[2] + $lineOffset;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($token[0], [T_ABSTRACT, T_FINAL])) {
|
||||
// skip
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
}
|
||||
|
||||
if ($token[0] === T_CLASS) {
|
||||
// Doc-comment before a class?
|
||||
if (is_array($previousToken) && $previousToken[0] === T_DOUBLE_COLON) {
|
||||
// php 5.5 class name resolution (i.e. ClassName::class)
|
||||
continue;
|
||||
}
|
||||
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
|
||||
if (is_string($token) && ($token === '(' || $token === '{')) {
|
||||
// php7 anonymous classes (i.e. new class() { public function foo() {} };)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_array($token) && ($token[1] === 'extends' || $token[1] === 'implements')) {
|
||||
// php7 anonymous classes with extends (i.e. new class() extends { public function foo() {} };)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_array($token)) {
|
||||
// PHP 8 named argument
|
||||
continue;
|
||||
}
|
||||
|
||||
$interfaceDefinition = false;
|
||||
$traitDefinition = false;
|
||||
$enumDefinition = false;
|
||||
|
||||
$schemaContext = new Context(['class' => $token[1], 'line' => $token[2]], $parseContext);
|
||||
if ($classDefinition) {
|
||||
$analysis->addClassDefinition($classDefinition);
|
||||
}
|
||||
$classDefinition = [
|
||||
'class' => $token[1],
|
||||
'extends' => null,
|
||||
'properties' => [],
|
||||
'methods' => [],
|
||||
'context' => $schemaContext,
|
||||
];
|
||||
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
|
||||
if ($token[0] === T_EXTENDS) {
|
||||
$schemaContext->extends = $this->parseNamespace($tokens, $token, $parseContext);
|
||||
$classDefinition['extends'] = $schemaContext->fullyQualifiedName($schemaContext->extends);
|
||||
}
|
||||
|
||||
if ($token[0] === T_IMPLEMENTS) {
|
||||
$schemaContext->implements = $this->parseNamespaceList($tokens, $token, $parseContext);
|
||||
$classDefinition['implements'] = array_map([$schemaContext, 'fullyQualifiedName'], $schemaContext->implements);
|
||||
}
|
||||
|
||||
if ($comment) {
|
||||
$schemaContext->line = $line;
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, $schemaContext);
|
||||
$comment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// @todo detect end-of-class and reset $schemaContext
|
||||
}
|
||||
|
||||
if ($token[0] === T_INTERFACE) { // Doc-comment before an interface?
|
||||
$classDefinition = false;
|
||||
$traitDefinition = false;
|
||||
$enumDefinition = false;
|
||||
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
|
||||
if (!is_array($token)) {
|
||||
// PHP 8 named argument
|
||||
continue;
|
||||
}
|
||||
|
||||
$schemaContext = new Context(['interface' => $token[1], 'line' => $token[2]], $parseContext);
|
||||
if ($interfaceDefinition) {
|
||||
$analysis->addInterfaceDefinition($interfaceDefinition);
|
||||
}
|
||||
$interfaceDefinition = [
|
||||
'interface' => $token[1],
|
||||
'extends' => null,
|
||||
'properties' => [],
|
||||
'methods' => [],
|
||||
'context' => $schemaContext,
|
||||
];
|
||||
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
|
||||
if ($token[0] === T_EXTENDS) {
|
||||
$schemaContext->extends = $this->parseNamespaceList($tokens, $token, $parseContext);
|
||||
$interfaceDefinition['extends'] = array_map([$schemaContext, 'fullyQualifiedName'], $schemaContext->extends);
|
||||
}
|
||||
|
||||
if ($comment) {
|
||||
$schemaContext->line = $line;
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, $schemaContext);
|
||||
$comment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// @todo detect end-of-interface and reset $schemaContext
|
||||
}
|
||||
|
||||
if ($token[0] === T_TRAIT) {
|
||||
$classDefinition = false;
|
||||
$interfaceDefinition = false;
|
||||
$enumDefinition = false;
|
||||
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
|
||||
if (!is_array($token)) {
|
||||
// PHP 8 named argument
|
||||
continue;
|
||||
}
|
||||
|
||||
$schemaContext = new Context(['trait' => $token[1], 'line' => $token[2]], $parseContext);
|
||||
if ($traitDefinition) {
|
||||
$analysis->addTraitDefinition($traitDefinition);
|
||||
}
|
||||
$traitDefinition = [
|
||||
'trait' => $token[1],
|
||||
'properties' => [],
|
||||
'methods' => [],
|
||||
'context' => $schemaContext,
|
||||
];
|
||||
|
||||
if ($comment) {
|
||||
$schemaContext->line = $line;
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, $schemaContext);
|
||||
$comment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// @todo detect end-of-trait and reset $schemaContext
|
||||
}
|
||||
|
||||
if (defined('T_ENUM') && $token[0] === T_ENUM) {
|
||||
$classDefinition = false;
|
||||
$interfaceDefinition = false;
|
||||
$traitDefinition = false;
|
||||
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
|
||||
if (!is_array($token)) {
|
||||
// PHP 8 named argument
|
||||
continue;
|
||||
}
|
||||
|
||||
$schemaContext = new Context(['enum' => $token[1], 'line' => $token[2]], $parseContext);
|
||||
if ($enumDefinition) {
|
||||
$analysis->addEnumDefinition($enumDefinition);
|
||||
}
|
||||
$enumDefinition = [
|
||||
'enum' => $token[1],
|
||||
'properties' => [],
|
||||
'methods' => [],
|
||||
'context' => $schemaContext,
|
||||
];
|
||||
|
||||
if ($comment) {
|
||||
$schemaContext->line = $line;
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, $schemaContext);
|
||||
$comment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// @todo detect end-of-trait and reset $schemaContext
|
||||
}
|
||||
|
||||
if ($token[0] === T_STATIC) {
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
if ($token[0] === T_VARIABLE) {
|
||||
// static property
|
||||
$propertyContext = new Context(
|
||||
[
|
||||
'property' => substr($token[1], 1),
|
||||
'static' => true,
|
||||
'line' => $line,
|
||||
],
|
||||
$schemaContext
|
||||
);
|
||||
|
||||
if ($classDefinition) {
|
||||
$classDefinition['properties'][$propertyContext->property] = $propertyContext;
|
||||
}
|
||||
if ($traitDefinition) {
|
||||
$traitDefinition['properties'][$propertyContext->property] = $propertyContext;
|
||||
}
|
||||
if ($comment) {
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, $propertyContext);
|
||||
$comment = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($token[0], [T_PRIVATE, T_PROTECTED, T_PUBLIC, T_VAR])) { // Scope
|
||||
[$type, $nullable, $token] = $this->parseTypeAndNextToken($tokens, $parseContext);
|
||||
if ($token[0] === T_VARIABLE) {
|
||||
// instance property
|
||||
$propertyContext = new Context(
|
||||
[
|
||||
'property' => substr($token[1], 1),
|
||||
'type' => $type,
|
||||
'nullable' => $nullable,
|
||||
'line' => $line,
|
||||
],
|
||||
$schemaContext
|
||||
);
|
||||
|
||||
if ($classDefinition) {
|
||||
$classDefinition['properties'][$propertyContext->property] = $propertyContext;
|
||||
}
|
||||
if ($interfaceDefinition) {
|
||||
$interfaceDefinition['properties'][$propertyContext->property] = $propertyContext;
|
||||
}
|
||||
if ($traitDefinition) {
|
||||
$traitDefinition['properties'][$propertyContext->property] = $propertyContext;
|
||||
}
|
||||
if ($comment) {
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, $propertyContext);
|
||||
$comment = false;
|
||||
}
|
||||
} elseif ($token[0] === T_FUNCTION) {
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
if ($token[0] === T_STRING) {
|
||||
$methodContext = new Context(
|
||||
[
|
||||
'method' => $token[1],
|
||||
'line' => $line,
|
||||
],
|
||||
$schemaContext
|
||||
);
|
||||
|
||||
if ($classDefinition) {
|
||||
$classDefinition['methods'][$token[1]] = $methodContext;
|
||||
}
|
||||
if ($interfaceDefinition) {
|
||||
$interfaceDefinition['methods'][$token[1]] = $methodContext;
|
||||
}
|
||||
if ($traitDefinition) {
|
||||
$traitDefinition['methods'][$token[1]] = $methodContext;
|
||||
}
|
||||
if ($comment) {
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, $methodContext);
|
||||
$comment = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} elseif ($token[0] === T_FUNCTION) {
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
if ($token[0] === T_STRING) {
|
||||
$methodContext = new Context(
|
||||
[
|
||||
'method' => $token[1],
|
||||
'line' => $line,
|
||||
],
|
||||
$schemaContext
|
||||
);
|
||||
|
||||
if ($classDefinition) {
|
||||
$classDefinition['methods'][$token[1]] = $methodContext;
|
||||
}
|
||||
if ($interfaceDefinition) {
|
||||
$interfaceDefinition['methods'][$token[1]] = $methodContext;
|
||||
}
|
||||
if ($traitDefinition) {
|
||||
$traitDefinition['methods'][$token[1]] = $methodContext;
|
||||
}
|
||||
if ($comment) {
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, $methodContext);
|
||||
$comment = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (in_array($token[0], [T_NAMESPACE, T_USE]) === false) {
|
||||
// Skip "use" & "namespace" to prevent "never imported" warnings)
|
||||
if ($comment) {
|
||||
// Not a doc-comment for a class, property or method?
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, new Context(['line' => $line], $schemaContext));
|
||||
$comment = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($token[0] === T_NAMESPACE) {
|
||||
$parseContext->namespace = $this->parseNamespace($tokens, $token, $parseContext);
|
||||
$aliases['__NAMESPACE__'] = $parseContext->namespace;
|
||||
$docBlockParser->setAliases($aliases);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($token[0] === T_USE) {
|
||||
$statements = $this->parseUseStatement($tokens, $token, $parseContext);
|
||||
foreach ($statements as $alias => $target) {
|
||||
if ($classDefinition) {
|
||||
// class traits
|
||||
$classDefinition['traits'][] = $schemaContext->fullyQualifiedName($target);
|
||||
} elseif ($traitDefinition) {
|
||||
// trait traits
|
||||
$traitDefinition['traits'][] = $schemaContext->fullyQualifiedName($target);
|
||||
} else {
|
||||
// not a trait use
|
||||
$parseContext->uses[$alias] = $target;
|
||||
|
||||
$namespaces = $generator->getNamespaces();
|
||||
if (null === $namespaces) {
|
||||
$aliases[strtolower($alias)] = $target;
|
||||
} else {
|
||||
foreach ($namespaces as $namespace) {
|
||||
if (strcasecmp(substr($target . '\\', 0, strlen($namespace)), $namespace) === 0) {
|
||||
$aliases[strtolower($alias)] = $target;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$docBlockParser->setAliases($aliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup final comment and definition
|
||||
if ($comment) {
|
||||
$this->analyseComment($analysis, $docBlockParser, $comment, new Context(['line' => $line], $schemaContext));
|
||||
}
|
||||
if ($classDefinition) {
|
||||
$analysis->addClassDefinition($classDefinition);
|
||||
}
|
||||
if ($interfaceDefinition) {
|
||||
$analysis->addInterfaceDefinition($interfaceDefinition);
|
||||
}
|
||||
if ($traitDefinition) {
|
||||
$analysis->addTraitDefinition($traitDefinition);
|
||||
}
|
||||
if ($enumDefinition) {
|
||||
$analysis->addEnumDefinition($enumDefinition);
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse comment and add annotations to analysis.
|
||||
*/
|
||||
private function analyseComment(Analysis $analysis, DocBlockParser $docBlockParser, string $comment, Context $context): void
|
||||
{
|
||||
$analysis->addAnnotations($docBlockParser->fromComment($comment, $context), $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* The next non-whitespace, non-comment token.
|
||||
*
|
||||
*
|
||||
* @return array|string The next token (or false)
|
||||
*/
|
||||
private function nextToken(array &$tokens, Context $context)
|
||||
{
|
||||
while (true) {
|
||||
$token = next($tokens);
|
||||
if (is_array($token)) {
|
||||
if ($token[0] === T_WHITESPACE) {
|
||||
continue;
|
||||
}
|
||||
if ($token[0] === T_COMMENT) {
|
||||
$pos = strpos($token[1], '@OA\\');
|
||||
if ($pos) {
|
||||
$line = $context->line ? $context->line + $token[2] : $token[2];
|
||||
$commentContext = new Context(['line' => $line], $context);
|
||||
$context->logger->warning('Annotations are only parsed inside `/**` DocBlocks, skipping ' . $commentContext);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseAttribute(array &$tokens, &$token, Context $parseContext): void
|
||||
{
|
||||
$nesting = 1;
|
||||
while ($token !== false) {
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
if (!is_array($token) && '[' === $token) {
|
||||
++$nesting;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_array($token) && ']' === $token) {
|
||||
--$nesting;
|
||||
if (!$nesting) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
private function php8NamespaceToken(): array
|
||||
{
|
||||
return defined('T_NAME_QUALIFIED') ? [T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse namespaced string.
|
||||
*
|
||||
* @param array|string $token
|
||||
*/
|
||||
private function parseNamespace(array &$tokens, &$token, Context $parseContext): string
|
||||
{
|
||||
$namespace = '';
|
||||
$nsToken = array_merge([T_STRING, T_NS_SEPARATOR], $this->php8NamespaceToken());
|
||||
while ($token !== false) {
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
if (!in_array($token[0], $nsToken)) {
|
||||
break;
|
||||
}
|
||||
$namespace .= $token[1];
|
||||
}
|
||||
|
||||
return $namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse comma separated list of namespaced strings.
|
||||
*
|
||||
* @param array|string $token
|
||||
*/
|
||||
private function parseNamespaceList(array &$tokens, &$token, Context $parseContext): array
|
||||
{
|
||||
$namespaces = [];
|
||||
while ($namespace = $this->parseNamespace($tokens, $token, $parseContext)) {
|
||||
$namespaces[] = $namespace;
|
||||
if ($token != ',') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $namespaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a use statement.
|
||||
*
|
||||
* @param (int|mixed)[]|string $token
|
||||
*/
|
||||
private function parseUseStatement(array &$tokens, &$token, Context $parseContext): array
|
||||
{
|
||||
$normalizeAlias = function ($alias): string {
|
||||
$alias = ltrim($alias, '\\');
|
||||
$elements = explode('\\', $alias);
|
||||
|
||||
return array_pop($elements);
|
||||
};
|
||||
|
||||
$class = '';
|
||||
$alias = '';
|
||||
$statements = [];
|
||||
$explicitAlias = false;
|
||||
$nsToken = array_merge([T_STRING, T_NS_SEPARATOR], $this->php8NamespaceToken());
|
||||
while ($token !== false) {
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
$isNameToken = in_array($token[0], $nsToken);
|
||||
if (!$explicitAlias && $isNameToken) {
|
||||
$class .= $token[1];
|
||||
$alias = $token[1];
|
||||
} elseif ($explicitAlias && $isNameToken) {
|
||||
$alias .= $token[1];
|
||||
} elseif ($token[0] === T_AS) {
|
||||
$explicitAlias = true;
|
||||
$alias = '';
|
||||
} elseif ($token === ',') {
|
||||
$statements[$normalizeAlias($alias)] = $class;
|
||||
$class = '';
|
||||
$alias = '';
|
||||
$explicitAlias = false;
|
||||
} elseif ($token === ';') {
|
||||
$statements[$normalizeAlias($alias)] = $class;
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $statements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse type of variable (if it exists).
|
||||
*/
|
||||
private function parseTypeAndNextToken(array &$tokens, Context $parseContext): array
|
||||
{
|
||||
$type = Generator::UNDEFINED;
|
||||
$nullable = false;
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
|
||||
if ($token[0] === T_STATIC) {
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
}
|
||||
|
||||
if ($token === '?') { // nullable type
|
||||
$nullable = true;
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
}
|
||||
|
||||
$qualifiedToken = array_merge([T_NS_SEPARATOR, T_STRING, T_ARRAY], $this->php8NamespaceToken());
|
||||
$typeToken = array_merge([T_STRING], $this->php8NamespaceToken());
|
||||
// drill down namespace segments to basename property type declaration
|
||||
while (in_array($token[0], $qualifiedToken)) {
|
||||
if (in_array($token[0], $typeToken)) {
|
||||
$type = $token[1];
|
||||
}
|
||||
$token = $this->nextToken($tokens, $parseContext);
|
||||
}
|
||||
|
||||
return [$type, $nullable, $token];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @license Apache 2.0
|
||||
*/
|
||||
|
||||
namespace OpenApi\Analysers;
|
||||
|
||||
/**
|
||||
* High level, PHP token based, scanner.
|
||||
*/
|
||||
class TokenScanner
|
||||
{
|
||||
/**
|
||||
* Scan file for all classes, interfaces and traits.
|
||||
*
|
||||
* @return string[][] File details
|
||||
*/
|
||||
public function scanFile(string $filename): array
|
||||
{
|
||||
return $this->scanTokens(token_get_all(file_get_contents($filename)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan file for all classes, interfaces and traits.
|
||||
*
|
||||
* @return array<string, array<string, mixed>> File details
|
||||
*/
|
||||
protected function scanTokens(array $tokens): array
|
||||
{
|
||||
$units = [];
|
||||
$uses = [];
|
||||
$isInterface = false;
|
||||
$isAbstractFunction = false;
|
||||
$namespace = '';
|
||||
$currentName = null;
|
||||
$unitLevel = 0;
|
||||
$lastToken = null;
|
||||
$stack = [];
|
||||
|
||||
$initUnit = function ($uses): array {
|
||||
return [
|
||||
'uses' => $uses,
|
||||
'interfaces' => [],
|
||||
'traits' => [],
|
||||
'enums' => [],
|
||||
'methods' => [],
|
||||
'properties' => [],
|
||||
];
|
||||
};
|
||||
|
||||
while (false !== ($token = $this->nextToken($tokens))) {
|
||||
// named arguments
|
||||
$nextToken = $this->nextToken($tokens);
|
||||
if (($token !== '}' && $nextToken === ':') || $nextToken === false) {
|
||||
continue;
|
||||
}
|
||||
do {
|
||||
$prevToken = prev($tokens);
|
||||
} while ($token !== $prevToken);
|
||||
|
||||
if (!is_array($token)) {
|
||||
switch ($token) {
|
||||
case '{':
|
||||
$stack[] = $token;
|
||||
break;
|
||||
case '}':
|
||||
array_pop($stack);
|
||||
if (count($stack) == $unitLevel) {
|
||||
$currentName = null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($token[0]) {
|
||||
case T_ABSTRACT:
|
||||
if (count($stack)) {
|
||||
$isAbstractFunction = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case T_CURLY_OPEN:
|
||||
case T_DOLLAR_OPEN_CURLY_BRACES:
|
||||
$stack[] = $token[1];
|
||||
break;
|
||||
|
||||
case T_NAMESPACE:
|
||||
$namespace = $this->nextWord($tokens);
|
||||
break;
|
||||
|
||||
case T_USE:
|
||||
if (!$stack) {
|
||||
$uses = array_merge($uses, $this->parseFQNStatement($tokens, $token));
|
||||
} elseif ($currentName) {
|
||||
$traits = $this->resolveFQN($this->parseFQNStatement($tokens, $token), $namespace, $uses);
|
||||
$units[$currentName]['traits'] = array_merge($units[$currentName]['traits'], $traits);
|
||||
}
|
||||
break;
|
||||
|
||||
case T_CLASS:
|
||||
if ($currentName) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ($lastToken && is_array($lastToken) && $lastToken[0] === T_DOUBLE_COLON) {
|
||||
// ::class
|
||||
break;
|
||||
}
|
||||
|
||||
// class name
|
||||
$token = $this->nextToken($tokens);
|
||||
|
||||
// unless ...
|
||||
if (is_string($token) && ($token === '(' || $token === '{')) {
|
||||
// new class[()] { ... }
|
||||
if ('{' == $token) {
|
||||
prev($tokens);
|
||||
}
|
||||
break;
|
||||
} elseif (is_array($token) && in_array($token[1], ['extends', 'implements'])) {
|
||||
// new class[()] extends { ... }
|
||||
break;
|
||||
}
|
||||
|
||||
$isInterface = false;
|
||||
$currentName = $namespace . '\\' . $token[1];
|
||||
$unitLevel = count($stack);
|
||||
$units[$currentName] = $initUnit($uses);
|
||||
break;
|
||||
|
||||
case T_INTERFACE:
|
||||
if ($currentName) {
|
||||
break;
|
||||
}
|
||||
|
||||
$isInterface = true;
|
||||
$token = $this->nextToken($tokens);
|
||||
$currentName = $namespace . '\\' . $token[1];
|
||||
$unitLevel = count($stack);
|
||||
$units[$currentName] = $initUnit($uses);
|
||||
break;
|
||||
|
||||
case T_EXTENDS:
|
||||
$fqns = $this->parseFQNStatement($tokens, $token);
|
||||
if ($isInterface && $currentName) {
|
||||
$units[$currentName]['interfaces'] = $this->resolveFQN($fqns, $namespace, $uses);
|
||||
}
|
||||
if (!is_array($token) || T_IMPLEMENTS !== $token[0]) {
|
||||
break;
|
||||
}
|
||||
// no break
|
||||
case T_IMPLEMENTS:
|
||||
$fqns = $this->parseFQNStatement($tokens, $token);
|
||||
if ($currentName) {
|
||||
$units[$currentName]['interfaces'] = $this->resolveFQN($fqns, $namespace, $uses);
|
||||
}
|
||||
break;
|
||||
|
||||
case T_FUNCTION:
|
||||
$token = $this->nextToken($tokens);
|
||||
if ((!is_array($token) && '&' == $token)
|
||||
|| (defined('T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG') && T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG == $token[0])) {
|
||||
$token = $this->nextToken($tokens);
|
||||
}
|
||||
|
||||
if (($unitLevel + 1) == count($stack) && $currentName) {
|
||||
$units[$currentName]['methods'][] = $token[1];
|
||||
if (!$isInterface && !$isAbstractFunction) {
|
||||
// more nesting
|
||||
$units[$currentName]['properties'] = array_merge(
|
||||
$units[$currentName]['properties'],
|
||||
$this->parsePromotedProperties($tokens)
|
||||
);
|
||||
$this->skipTo($tokens, '{', true);
|
||||
} else {
|
||||
// no function body
|
||||
$this->skipTo($tokens, ';');
|
||||
$isAbstractFunction = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case T_VARIABLE:
|
||||
if (($unitLevel + 1) == count($stack) && $currentName) {
|
||||
$units[$currentName]['properties'][] = substr($token[1], 1);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// handle trait here too to avoid duplication
|
||||
if (T_TRAIT === $token[0] || (defined('T_ENUM') && T_ENUM === $token[0])) {
|
||||
if ($currentName) {
|
||||
break;
|
||||
}
|
||||
|
||||
$isInterface = false;
|
||||
$token = $this->nextToken($tokens);
|
||||
$currentName = $namespace . '\\' . $token[1];
|
||||
$unitLevel = count($stack);
|
||||
$this->skipTo($tokens, '{', true);
|
||||
$units[$currentName] = $initUnit($uses);
|
||||
}
|
||||
break;
|
||||
}
|
||||
$lastToken = $token;
|
||||
}
|
||||
|
||||
return $units;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next token that is not whitespace or comment.
|
||||
*
|
||||
* @return string|array|false
|
||||
*/
|
||||
protected function nextToken(array &$tokens)
|
||||
{
|
||||
$token = true;
|
||||
while ($token) {
|
||||
$token = next($tokens);
|
||||
if (is_array($token)) {
|
||||
if (in_array($token[0], [T_WHITESPACE, T_COMMENT])) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
protected function resolveFQN(array $names, string $namespace, array $uses): array
|
||||
{
|
||||
$resolve = function ($name) use ($namespace, $uses) {
|
||||
if ('\\' == $name[0]) {
|
||||
return substr($name, 1);
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $uses)) {
|
||||
return $uses[$name];
|
||||
}
|
||||
|
||||
return $namespace . '\\' . $name;
|
||||
};
|
||||
|
||||
return array_values(array_map($resolve, $names));
|
||||
}
|
||||
|
||||
protected function skipTo(array &$tokens, string $char, bool $prev = false): void
|
||||
{
|
||||
while (false !== ($token = next($tokens))) {
|
||||
if (is_string($token) && $token == $char) {
|
||||
if ($prev) {
|
||||
prev($tokens);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read next word.
|
||||
*
|
||||
* Skips leading whitespace.
|
||||
*/
|
||||
protected function nextWord(array &$tokens): string
|
||||
{
|
||||
$word = '';
|
||||
while (false !== ($token = next($tokens))) {
|
||||
if (is_array($token)) {
|
||||
if ($token[0] === T_WHITESPACE) {
|
||||
if ($word) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$word .= $token[1];
|
||||
}
|
||||
}
|
||||
|
||||
return $word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a use statement.
|
||||
*/
|
||||
protected function parseFQNStatement(array &$tokens, array &$token): array
|
||||
{
|
||||
$normalizeAlias = function ($alias): string {
|
||||
$alias = ltrim($alias, '\\');
|
||||
$elements = explode('\\', $alias);
|
||||
|
||||
return array_pop($elements);
|
||||
};
|
||||
|
||||
$class = '';
|
||||
$alias = '';
|
||||
$statements = [];
|
||||
$explicitAlias = false;
|
||||
$php8NSToken = defined('T_NAME_QUALIFIED') ? [T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED] : [];
|
||||
$nsToken = array_merge([T_STRING, T_NS_SEPARATOR], $php8NSToken);
|
||||
while ($token !== false) {
|
||||
$token = $this->nextToken($tokens);
|
||||
$isNameToken = in_array($token[0], $nsToken);
|
||||
if (!$explicitAlias && $isNameToken) {
|
||||
$class .= $token[1];
|
||||
$alias = $token[1];
|
||||
} elseif ($explicitAlias && $isNameToken) {
|
||||
$alias .= $token[1];
|
||||
} elseif ($token[0] === T_AS) {
|
||||
$explicitAlias = true;
|
||||
$alias = '';
|
||||
} elseif ($token[0] === T_IMPLEMENTS) {
|
||||
$statements[$normalizeAlias($alias)] = $class;
|
||||
break;
|
||||
} elseif ($token === ',') {
|
||||
$statements[$normalizeAlias($alias)] = $class;
|
||||
$class = '';
|
||||
$alias = '';
|
||||
$explicitAlias = false;
|
||||
} elseif ($token === ';') {
|
||||
$statements[$normalizeAlias($alias)] = $class;
|
||||
break;
|
||||
} elseif ($token === '{') {
|
||||
$statements[$normalizeAlias($alias)] = $class;
|
||||
prev($tokens);
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $statements;
|
||||
}
|
||||
|
||||
protected function parsePromotedProperties(array &$tokens): array
|
||||
{
|
||||
$properties = [];
|
||||
|
||||
$this->skipTo($tokens, '(');
|
||||
$round = 1;
|
||||
$promoted = false;
|
||||
while (false !== ($token = $this->nextToken($tokens))) {
|
||||
if (is_string($token)) {
|
||||
switch ($token) {
|
||||
case '(':
|
||||
++$round;
|
||||
break;
|
||||
case ')':
|
||||
--$round;
|
||||
if (0 == $round) {
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (is_array($token)) {
|
||||
switch ($token[0]) {
|
||||
case T_PUBLIC:
|
||||
case T_PROTECTED:
|
||||
case T_PRIVATE:
|
||||
$promoted = true;
|
||||
break;
|
||||
case T_VARIABLE:
|
||||
if ($promoted) {
|
||||
$properties[] = ltrim($token[1], '$');
|
||||
$promoted = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user