diff --git a/src/Analysers/TokenScanner.php b/src/Analysers/TokenScanner.php index 239a03018..d0baa0d83 100644 --- a/src/Analysers/TokenScanner.php +++ b/src/Analysers/TokenScanner.php @@ -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]) { @@ -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 { ... } @@ -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: @@ -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; } @@ -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; } } diff --git a/tests/Analysers/TokenAnalyserTest.php b/tests/Analysers/TokenAnalyserTest.php index e8d0c718b..87fd1aec1 100644 --- a/tests/Analysers/TokenAnalyserTest.php +++ b/tests/Analysers/TokenAnalyserTest.php @@ -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; @@ -130,7 +131,7 @@ 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 @@ -138,7 +139,7 @@ public function testThirdPartyAnnotations() $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); @@ -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); + } } diff --git a/tests/Analysers/TokenScannerTest.php b/tests/Analysers/TokenScannerTest.php index 2cd7df702..1b04d09d4 100644 --- a/tests/Analysers/TokenScannerTest.php +++ b/tests/Analysers/TokenScannerTest.php @@ -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' => [], @@ -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' => [], @@ -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'], @@ -151,7 +151,7 @@ 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'], @@ -159,6 +159,18 @@ public function scanCases() ], ], ], + 'AnonymousFunctions' => [ + 'PHP/AnonymousFunctions.php', + [ + 'OpenApi\\Tests\\Fixtures\\PHP\\AnonymousFunctions' => [ + 'uses' => ['Info' => 'OpenApi\\Annotations\\Info'], + 'interfaces' => [], + 'traits' => [], + 'methods' => ['index', 'query', 'other'], + 'properties' => [], + ], + ], + ], ]; } diff --git a/tests/AnalysisTest.php b/tests/AnalysisTest.php index 3121cd9c0..eb43b4eaa 100644 --- a/tests/AnalysisTest.php +++ b/tests/AnalysisTest.php @@ -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')) ); } diff --git a/tests/CommandlineInterfaceTest.php b/tests/CommandlineInterfaceTest.php index 7fdc3c9c0..8c56611ed 100644 --- a/tests/CommandlineInterfaceTest.php +++ b/tests/CommandlineInterfaceTest.php @@ -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); @@ -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); @@ -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); @@ -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); diff --git a/tests/ContextTest.php b/tests/ContextTest.php index d366ecb3a..18818b095 100644 --- a/tests/ContextTest.php +++ b/tests/ContextTest.php @@ -17,11 +17,11 @@ 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() @@ -29,7 +29,7 @@ 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')); diff --git a/tests/ExamplesTest.php b/tests/ExamplesTest.php index 84d621897..e5105232f 100644 --- a/tests/ExamplesTest.php +++ b/tests/ExamplesTest.php @@ -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); diff --git a/tests/Fixtures/PHP/AnonymousFunctions.php b/tests/Fixtures/PHP/AnonymousFunctions.php new file mode 100644 index 000000000..c7e9eafd9 --- /dev/null +++ b/tests/Fixtures/PHP/AnonymousFunctions.php @@ -0,0 +1,43 @@ +query() + ->leftJoin('foo', function ($join) { + $join->on('user.foo_id', 'foo.id'); + }) + ->leftJoin('bar', function ($join) { + $join->on('user.bar_id', 'bar.id'); + }) + ->get(); + } +} diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php index c982356e5..3bf552779 100644 --- a/tests/GeneratorTest.php +++ b/tests/GeneratorTest.php @@ -14,11 +14,9 @@ class GeneratorTest extends OpenApiTestCase { - const SOURCE_DIR = __DIR__ . '/../Examples/swagger-spec/petstore-simple'; - - public function sourcesProvider() + public function sourcesProvider(): iterable { - $sourceDir = self::SOURCE_DIR; + $sourceDir = $this->example('swagger-spec/petstore-simple'); yield 'dir-list' => [$sourceDir, [$sourceDir]]; yield 'file-list' => [$sourceDir, ["$sourceDir/SimplePet.php", "$sourceDir/SimplePetsController.php", "$sourceDir/api.php"]]; @@ -38,7 +36,7 @@ public function testScan(string $sourceDir, iterable $sources) $this->assertSpecEquals(file_get_contents(sprintf('%s/%s.yaml', $sourceDir, basename($sourceDir))), $openapi); } - public function processorCases() + public function processorCases(): iterable { return [ [new OperationId(false), false], diff --git a/tests/OpenApiTestCase.php b/tests/OpenApiTestCase.php index 5c58f2376..093ada315 100644 --- a/tests/OpenApiTestCase.php +++ b/tests/OpenApiTestCase.php @@ -182,6 +182,11 @@ protected function createOpenApiWithInfo(): OpenApi ]); } + public function example(string $name): string + { + return __DIR__ . '/../Examples/' . $name; + } + public function fixture(string $file): ?string { $fixtures = $this->fixtures([$file]); @@ -198,7 +203,7 @@ public function fixtures(array $files): array { return array_map(function ($file) { return __DIR__ . '/Fixtures/' . $file; - }, (array) $files); + }, $files); } public function analysisFromFixtures(array $files, array $processors = [], ?AnalyserInterface $analyzer = null): Analysis diff --git a/tests/SerializerTest.php b/tests/SerializerTest.php index 5d83f6036..7d367bab6 100644 --- a/tests/SerializerTest.php +++ b/tests/SerializerTest.php @@ -149,7 +149,7 @@ public function testDeserializeAnnotation() public function testPetstoreExample() { $serializer = new Serializer(); - $spec = __DIR__ . '/../Examples/petstore.swagger.io/petstore.swagger.io.json'; + $spec = $this->example('petstore.swagger.io/petstore.swagger.io.json'); $openapi = $serializer->deserializeFile($spec); $this->assertInstanceOf(OpenApi::class, $openapi); $this->assertJsonStringEqualsJsonString(file_get_contents($spec), $openapi->toJson()); diff --git a/tests/UtilTest.php b/tests/UtilTest.php index 341542f48..2316950bb 100644 --- a/tests/UtilTest.php +++ b/tests/UtilTest.php @@ -33,7 +33,7 @@ public function testExclude() ]; $openapi = (new Generator()) ->setAnalyser(new TokenAnalyser()) - ->generate(Util::finder(__DIR__ . '/Fixtures', $exclude)); + ->generate(Util::finder($this->fixture(''), $exclude)); $this->assertSame('Fixture for ParserTest', $openapi->info->title, 'No errors about duplicate @OA\Info() annotations'); } @@ -50,10 +50,10 @@ public function testRefDecode() public function testFinder() { // Create a finder for one of the example directories that has a subdirectory. - $finder = (new Finder())->in(__DIR__ . '/../Examples/using-traits'); + $finder = (new Finder())->in($this->example('using-traits')); $this->assertGreaterThan(0, iterator_count($finder), 'There should be at least a few files and a directory.'); $finder_array = \iterator_to_array($finder); - $directory_path = __DIR__ . '/../Examples/using-traits/Decoration'; + $directory_path = $this->example('using-traits/Decoration'); $this->assertArrayHasKey($directory_path, $finder_array, 'The directory should be a path in the finder.'); // Use the Util method that should set the finder to only find files, since swagger-php only needs files. $finder_result = Util::finder($finder);