welcome back to dyb-tech

This commit is contained in:
Daniel Guzman
2024-05-18 02:28:01 +02:00
parent 9513cdba09
commit 9f30bc98c7
6149 changed files with 668407 additions and 0 deletions
@@ -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;
}
}