Skip to content

Commit

Permalink
Parse anonymous functions (#1011)
Browse files Browse the repository at this point in the history
Clean up token scanner.
  • Loading branch information
DerManoMann authored Dec 6, 2021
1 parent f2984cb commit 7e9ba95
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 56 deletions.
38 changes: 16 additions & 22 deletions src/Analysers/TokenScanner.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,25 +34,18 @@ protected function scanTokens(array $tokens): array
$namespace = '';
$currentName = null;
$lastToken = null;
$curlyNested = 0;
$stack = [];

while (false !== ($token = $this->nextToken($tokens))) {
if (!is_array($token)) {
switch ($token) {
case '{':
++$curlyNested;
$stack[] = $token;
break;
case '}':
--$curlyNested;
array_pop($stack);
break;
}
if ($stack) {
$last = array_pop($stack);
if ($last[1] < $curlyNested) {
$stack[] = $last;
}
}
continue;
}
switch ($token[0]) {
Expand All @@ -79,6 +72,9 @@ protected function scanTokens(array $tokens): array
// 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 { ... }
Expand All @@ -87,22 +83,19 @@ protected function scanTokens(array $tokens): array

$isInterface = false;
$currentName = $namespace . '\\' . $token[1];
$stack[] = [$currentName, $curlyNested];
$units[$currentName] = ['uses' => $uses, 'interfaces' => [], 'traits' => [], 'methods' => [], 'properties' => []];
break;
case T_INTERFACE:
$isInterface = true;
$token = $this->nextToken($tokens);
$currentName = $namespace . '\\' . $token[1];
$stack[] = [$currentName, $curlyNested];
$units[$currentName] = ['uses' => $uses, 'interfaces' => [], 'traits' => [], 'methods' => [], 'properties' => []];
break;
case T_TRAIT:
$isInterface = false;
$token = $this->nextToken($tokens);
$currentName = $namespace . '\\' . $token[1];
$this->skipTo($tokens, '{');
$stack[] = [$currentName, $curlyNested++];
$this->skipTo($tokens, '{', true);
$units[$currentName] = ['uses' => $uses, 'interfaces' => [], 'traits' => [], 'methods' => [], 'properties' => []];
break;
case T_EXTENDS:
Expand All @@ -123,24 +116,21 @@ protected function scanTokens(array $tokens): array
case T_FUNCTION:
$token = $this->nextToken($tokens);

if (1 == count($stack)) {
$name = $stack[0][0];
if (1 == count($stack) && $currentName) {
if (!$isInterface) {
// more nesting
$this->skipTo($tokens, '{');
$stack[] = [$token[1], ++$curlyNested];
$this->skipTo($tokens, '{', true);
} else {
// no function body
$this->skipTo($tokens, ';');
}

$units[$name]['methods'][] = $token[1];
$units[$currentName]['methods'][] = $token[1];
}
break;
case T_VARIABLE:
if (1 == count($stack)) {
$name = $stack[0][0];
$units[$name]['properties'][] = substr($token[1], 1);
if (1 == count($stack) && $currentName) {
$units[$currentName]['properties'][] = substr($token[1], 1);
}
break;
}
Expand Down Expand Up @@ -187,10 +177,14 @@ protected function resolveFQN(array $names, string $namespace, array $uses): arr
return array_values(array_map($resolve, $names));
}

protected function skipTo(array &$tokens, $char): void
protected function skipTo(array &$tokens, $char, bool $prev = false): void
{
while (false !== ($token = next($tokens))) {
if (is_string($token) && $token == $char) {
if ($prev) {
prev($tokens);
}

break;
}
}
Expand Down
14 changes: 12 additions & 2 deletions tests/Analysers/TokenAnalyserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
namespace OpenApi\Tests\Analysers;

use OpenApi\Analysis;
use OpenApi\Annotations\Info;
use OpenApi\Annotations\Property;
use OpenApi\Annotations\Schema;
use OpenApi\Generator;
Expand Down Expand Up @@ -130,15 +131,15 @@ public function testThirdPartyAnnotations()
$generator = new Generator();
$analyser = new TokenAnalyser();
$analyser->setGenerator($generator);
$defaultAnalysis = $analyser->fromFile(__DIR__ . '/../Fixtures/ThirdPartyAnnotations.php', $this->getContext());
$defaultAnalysis = $analyser->fromFile($this->fixture('ThirdPartyAnnotations.php'), $this->getContext());
$this->assertCount(3, $defaultAnalysis->annotations, 'Only read the @OA annotations, skip the others.');

// Allow the analyser to parse 3rd party annotations, which might
// contain useful info that could be extracted with a custom processor
$generator->addNamespace('AnotherNamespace\\Annotations\\');
$openapi = $generator
->setAnalyser(new TokenAnalyser())
->generate([__DIR__ . '/../Fixtures/ThirdPartyAnnotations.php']);
->generate([$this->fixture('ThirdPartyAnnotations.php')]);
$this->assertSame('api/3rd-party', $openapi->paths[0]->path);
$this->assertCount(4, $openapi->_unmerged);

Expand Down Expand Up @@ -268,4 +269,13 @@ public function testPhp8NamedProperty()
$this->assertCount(1, $properties);
$this->assertEquals('labels', $properties[0]->property);
}

public function testAnonymousFunctions()
{
$analysis = $this->analysisFromFixtures(['PHP/AnonymousFunctions.php'], [], new TokenAnalyser());
$analysis->process((new Generator())->getProcessors());

$infos = $analysis->getAnnotationsOfType(Info::class, true);
$this->assertCount(1, $infos);
}
}
30 changes: 21 additions & 9 deletions tests/Analysers/TokenScannerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,35 +18,35 @@ public function scanCases()
'Apis/DocBlocks/basic.php',
[
'OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\OpenApiSpec' => [
'uses' => ['OA' => 'OpenApi\Annotations'],
'uses' => ['OA' => 'OpenApi\\Annotations'],
'interfaces' => [],
'traits' => [],
'methods' => [],
'properties' => [],
],
'OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\Product' => [
'uses' => ['OA' => 'OpenApi\Annotations'],
'uses' => ['OA' => 'OpenApi\\Annotations'],
'interfaces' => ['OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\ProductInterface'],
'traits' => ['OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\NameTrait'],
'methods' => [],
'properties' => ['id'],
],
'OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\ProductController' => [
'uses' => ['OA' => 'OpenApi\Annotations'],
'uses' => ['OA' => 'OpenApi\\Annotations'],
'interfaces' => [],
'traits' => [],
'methods' => ['getProduct', 'addProduct'],
'properties' => [],
],
'OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\ProductInterface' => [
'uses' => ['OA' => 'OpenApi\Annotations'],
'uses' => ['OA' => 'OpenApi\\Annotations'],
'interfaces' => [],
'traits' => [],
'methods' => [],
'properties' => [],
],
'OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\NameTrait' => [
'uses' => ['OA' => 'OpenApi\Annotations'],
'uses' => ['OA' => 'OpenApi\\Annotations'],
'interfaces' => [],
'traits' => [],
'methods' => [],
Expand All @@ -61,14 +61,14 @@ public function scanCases()
'php8' => [
'PHP/php8.php',
[
'OpenApi\\Tests\Fixtures\\PHP\\MethodAttr' => [
'OpenApi\\Tests\\Fixtures\\PHP\\MethodAttr' => [
'uses' => [],
'interfaces' => [],
'traits' => [],
'methods' => [],
'properties' => [],
],
'OpenApi\Tests\\Fixtures\\PHP\\GenericAttr' => [
'OpenApi\\Tests\\Fixtures\\PHP\\GenericAttr' => [
'uses' => [],
'interfaces' => [],
'traits' => [],
Expand Down Expand Up @@ -112,7 +112,7 @@ public function scanCases()
'CustomerInterface.php',
[
'OpenApi\\Tests\\Fixtures\\CustomerInterface' => [
'uses' => ['OA' => 'OpenApi\Annotations'],
'uses' => ['OA' => 'OpenApi\\Annotations'],
'interfaces' => [],
'traits' => [],
'methods' => ['firstname', 'secondname', 'thirdname', 'fourthname', 'lastname', 'tags', 'submittedBy', 'friends', 'bestFriend'],
Expand Down Expand Up @@ -151,14 +151,26 @@ public function scanCases()
'PHP/Php8NamedProperty.php',
[
'OpenApi\\Tests\\Fixtures\\PHP\\Php8NamedProperty' => [
'uses' => ['Label' => 'OpenApi\Tests\Fixtures\PHP\Label'],
'uses' => ['Label' => 'OpenApi\\Tests\\Fixtures\\PHP\\Label'],
'interfaces' => [],
'traits' => [],
'methods' => ['__construct'],
'properties' => [],
],
],
],
'AnonymousFunctions' => [
'PHP/AnonymousFunctions.php',
[
'OpenApi\\Tests\\Fixtures\\PHP\\AnonymousFunctions' => [
'uses' => ['Info' => 'OpenApi\\Annotations\\Info'],
'interfaces' => [],
'traits' => [],
'methods' => ['index', 'query', 'other'],
'properties' => [],
],
],
],
];
}

Expand Down
6 changes: 3 additions & 3 deletions tests/AnalysisTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ public function testGetSubclasses()

$this->assertCount(3, $analysis->classes, '3 classes should\'ve been detected');

$subclasses = $analysis->getSubClasses('\OpenApi\Tests\Fixtures\ExpandClasses\GrandAncestor');
$subclasses = $analysis->getSubClasses('\\OpenApi\\Tests\\Fixtures\\ExpandClasses\\GrandAncestor');
$this->assertCount(2, $subclasses, 'GrandAncestor has 2 subclasses');
$this->assertSame(
['\OpenApi\Tests\Fixtures\ExpandClasses\Ancestor', '\AnotherNamespace\Child'],
['\\OpenApi\\Tests\\Fixtures\\ExpandClasses\\Ancestor', '\\AnotherNamespace\\Child'],
array_keys($subclasses)
);
$this->assertSame(
['\AnotherNamespace\Child'],
array_keys($analysis->getSubClasses('\OpenApi\Tests\Fixtures\ExpandClasses\Ancestor'))
array_keys($analysis->getSubClasses('\\OpenApi\\Tests\\Fixtures\\ExpandClasses\\Ancestor'))
);
}

Expand Down
10 changes: 5 additions & 5 deletions tests/CommandlineInterfaceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ protected function setUp(): void

public function testStdout()
{
$path = __DIR__ . '/../Examples/swagger-spec/petstore-simple';
$path = $this->example('swagger-spec/petstore-simple');
exec(__DIR__ . '/../bin/openapi --format yaml ' . escapeshellarg($path) . ' 2> /dev/null', $output, $retval);
$this->assertSame(0, $retval);
$yaml = implode(PHP_EOL, $output);
Expand All @@ -24,7 +24,7 @@ public function testStdout()

public function testOutputTofile()
{
$path = __DIR__ . '/../Examples/swagger-spec/petstore-simple';
$path = $this->example('swagger-spec/petstore-simple');
$filename = sys_get_temp_dir() . '/swagger-php-clitest.yaml';
exec(__DIR__ . '/../bin/openapi --format yaml -o ' . escapeshellarg($filename) . ' ' . escapeshellarg($path) . ' 2> /dev/null', $output, $retval);
$this->assertSame(0, $retval);
Expand All @@ -36,14 +36,14 @@ public function testOutputTofile()

public function testAddProcessor()
{
$path = __DIR__ . '/../Examples/swagger-spec/petstore-simple';
$path = $this->example('swagger-spec/petstore-simple');
exec(__DIR__ . '/../bin/openapi --processor OperationId --format yaml ' . escapeshellarg($path) . ' 2> /dev/null', $output, $retval);
$this->assertSame(0, $retval);
}

public function testExcludeListWarning()
{
$path = __DIR__ . '/../Examples/swagger-spec/petstore-simple';
$path = $this->example('swagger-spec/petstore-simple');
exec(__DIR__ . '/../bin/openapi -e foo,bar ' . escapeshellarg($path) . ' 2>&1', $output, $retval);
$this->assertSame(1, $retval);
$output = implode(PHP_EOL, $output);
Expand All @@ -52,7 +52,7 @@ public function testExcludeListWarning()

public function testMissingArg()
{
$path = __DIR__ . '/../Examples/swagger-spec/petstore-simple';
$path = $this->example('swagger-spec/petstore-simple');
exec(__DIR__ . '/../bin/openapi ' . escapeshellarg($path) . ' -e 2>&1', $output, $retval);
$this->assertSame(1, $retval);
$output = implode(PHP_EOL, $output);
Expand Down
6 changes: 3 additions & 3 deletions tests/ContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ public function testDetect()
$context = Context::detect();
$line = __LINE__ - 1;
$this->assertSame('ContextTest', $context->class);
$this->assertSame('\OpenApi\Tests\ContextTest', $context->fullyQualifiedName($context->class));
$this->assertSame('\\OpenApi\\Tests\\ContextTest', $context->fullyQualifiedName($context->class));
$this->assertSame('testDetect', $context->method);
$this->assertSame(__FILE__, $context->filename);
$this->assertSame($line, $context->line);
$this->assertSame('OpenApi\Tests', $context->namespace);
$this->assertSame('OpenApi\\Tests', $context->namespace);
}

public function testFullyQualifiedName()
{
$this->assertOpenApiLogEntryContains('Required @OA\PathItem() not found');
$openapi = (new Generator($this->getTrackingLogger()))
->setAnalyser(new TokenAnalyser())
->generate([__DIR__ . '/Fixtures/Customer.php']);
->generate([$this->fixture('Customer.php')]);
$context = $openapi->components->schemas[0]->_context;
// resolve with namespace
$this->assertSame('\FullyQualified', $context->fullyQualifiedName('\FullyQualified'));
Expand Down
4 changes: 2 additions & 2 deletions tests/ExamplesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,13 @@ public function exampleMappings()
public function testExamples($example, $spec, $analyser)
{
// register autoloader for examples that require autoloading due to inheritance, etc.
$path = __DIR__ . '/../Examples/' . $example;
$path = $this->example($example);
$exampleNS = str_replace(' ', '', ucwords(str_replace(['-', '.'], ' ', $example)));
$classloader = new ClassLoader();
$classloader->addPsr4('OpenApi\\Examples\\' . $exampleNS . '\\', $path);
$classloader->register();

$path = __DIR__ . '/../Examples/' . $example;
$path = $this->example($example);
$openapi = (new Generator())
->setAnalyser($analyser)
->generate([$path], null, true);
Expand Down
43 changes: 43 additions & 0 deletions tests/Fixtures/PHP/AnonymousFunctions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Tests\Fixtures\PHP;

use OpenApi\Annotations\Info;

/**
* @OA\Info(title="Foobar", version="1.0")
*/
class AnonymousFunctions
{
public function index()
{
array_map(function ($item) {
return '';
}, []);
}

protected function query()
{
return new class() {
public function leftJoin(string $foo, callable $callback) {
return $this;
}
};
}

public function other()
{
return $this->query()
->leftJoin('foo', function ($join) {
$join->on('user.foo_id', 'foo.id');
})
->leftJoin('bar', function ($join) {
$join->on('user.bar_id', 'bar.id');
})
->get();
}
}
Loading

0 comments on commit 7e9ba95

Please sign in to comment.