From 6bfe1c422a676c425ce9280f707015f0deebf254 Mon Sep 17 00:00:00 2001 From: Martin Rademacher Date: Fri, 10 Dec 2021 13:03:49 +1300 Subject: [PATCH] Fix token scanning/parsing for PHP8 named arguments (#1019) In particular handle argument names matching reserved words like 'class', 'interface' or 'trait'! --- src/Analysers/TokenAnalyser.php | 17 +++++++++++++++++ src/Analysers/TokenScanner.php | 22 +++++++++++++++++++++- tests/Analysers/TokenAnalyserTest.php | 12 ++++++++++++ tests/Analysers/TokenScannerTest.php | 12 ++++++++++++ tests/Fixtures/PHP/Php8NamedArguments.php | 23 +++++++++++++++++++++++ tests/Fixtures/PHP/php7.php | 6 +++++- 6 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/PHP/Php8NamedArguments.php diff --git a/src/Analysers/TokenAnalyser.php b/src/Analysers/TokenAnalyser.php index 87af0d8a4..3d091b0e5 100644 --- a/src/Analysers/TokenAnalyser.php +++ b/src/Analysers/TokenAnalyser.php @@ -135,6 +135,11 @@ protected function fromTokens(array $tokens, Context $parseContext): Analysis continue; } + if (!is_array($token)) { + // PHP 8 named argument + continue; + } + $interfaceDefinition = false; $traitDefinition = false; @@ -177,6 +182,12 @@ protected function fromTokens(array $tokens, Context $parseContext): Analysis $traitDefinition = 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); @@ -211,6 +222,12 @@ protected function fromTokens(array $tokens, Context $parseContext): Analysis $interfaceDefinition = 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); diff --git a/src/Analysers/TokenScanner.php b/src/Analysers/TokenScanner.php index a1bd90880..4cf70cd9a 100644 --- a/src/Analysers/TokenScanner.php +++ b/src/Analysers/TokenScanner.php @@ -48,14 +48,17 @@ protected function scanTokens(array $tokens): array } continue; } + switch ($token[0]) { 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)); @@ -64,7 +67,12 @@ protected function scanTokens(array $tokens): array $units[$currentName]['traits'] = array_merge($units[$currentName]['traits'], $traits); } break; + case T_CLASS: + if ($stack) { + break; + } + if ($lastToken && is_array($lastToken) && $lastToken[0] === T_DOUBLE_COLON) { // ::class break; @@ -89,19 +97,30 @@ protected function scanTokens(array $tokens): array $currentName = $namespace . '\\' . $token[1]; $units[$currentName] = ['uses' => $uses, 'interfaces' => [], 'traits' => [], 'methods' => [], 'properties' => []]; break; + case T_INTERFACE: + if ($stack) { + break; + } + $isInterface = true; $token = $this->nextToken($tokens); $currentName = $namespace . '\\' . $token[1]; $units[$currentName] = ['uses' => $uses, 'interfaces' => [], 'traits' => [], 'methods' => [], 'properties' => []]; break; + case T_TRAIT: + if ($stack) { + break; + } + $isInterface = false; $token = $this->nextToken($tokens); $currentName = $namespace . '\\' . $token[1]; $this->skipTo($tokens, '{', true); $units[$currentName] = ['uses' => $uses, 'interfaces' => [], 'traits' => [], 'methods' => [], 'properties' => []]; break; + case T_EXTENDS: $fqns = $this->parseFQNStatement($tokens, $token); if ($isInterface && $currentName) { @@ -117,6 +136,7 @@ protected function scanTokens(array $tokens): array $units[$currentName]['interfaces'] = $this->resolveFQN($fqns, $namespace, $uses); } break; + case T_FUNCTION: $token = $this->nextToken($tokens); @@ -128,10 +148,10 @@ protected function scanTokens(array $tokens): array // no function body $this->skipTo($tokens, ';'); } - $units[$currentName]['methods'][] = $token[1]; } break; + case T_VARIABLE: if (1 == count($stack) && $currentName) { $units[$currentName]['properties'][] = substr($token[1], 1); diff --git a/tests/Analysers/TokenAnalyserTest.php b/tests/Analysers/TokenAnalyserTest.php index 87fd1aec1..4e2259f44 100644 --- a/tests/Analysers/TokenAnalyserTest.php +++ b/tests/Analysers/TokenAnalyserTest.php @@ -278,4 +278,16 @@ public function testAnonymousFunctions() $infos = $analysis->getAnnotationsOfType(Info::class, true); $this->assertCount(1, $infos); } + + /** + * @requires PHP 8 + */ + public function testPhp8NamedArguments() + { + $analysis = $this->analysisFromFixtures(['PHP/Php8NamedArguments.php'], [], new TokenAnalyser()); + $schemas = $analysis->getAnnotationsOfType(Schema::class, true); + + $this->assertCount(1, $schemas); + $analysis->process((new Generator())->getProcessors()); + } } diff --git a/tests/Analysers/TokenScannerTest.php b/tests/Analysers/TokenScannerTest.php index a3232dc0b..993a1f466 100644 --- a/tests/Analysers/TokenScannerTest.php +++ b/tests/Analysers/TokenScannerTest.php @@ -159,6 +159,18 @@ public function scanCases() ], ], ], + 'Php8NamedArguments' => [ + 'PHP/Php8NamedArguments.php', + [ + 'OpenApi\\Tests\\Fixtures\\PHP\\Php8NamedArguments' => [ + 'uses' => [], + 'interfaces' => [], + 'traits' => [], + 'methods' => ['useFoo', 'foo'], + 'properties' => [], + ], + ], + ], 'AnonymousFunctions' => [ 'PHP/AnonymousFunctions.php', [ diff --git a/tests/Fixtures/PHP/Php8NamedArguments.php b/tests/Fixtures/PHP/Php8NamedArguments.php new file mode 100644 index 000000000..4caa003e3 --- /dev/null +++ b/tests/Fixtures/PHP/Php8NamedArguments.php @@ -0,0 +1,23 @@ +foo(class: 'abc', interface: 'def', trait: 'xyz'); + } + + public function foo(string $class, string $interface, string $trait): void + { + + } +} diff --git a/tests/Fixtures/PHP/php7.php b/tests/Fixtures/PHP/php7.php index 280906d00..074d1172d 100644 --- a/tests/Fixtures/PHP/php7.php +++ b/tests/Fixtures/PHP/php7.php @@ -6,6 +6,8 @@ namespace OpenApi\Tests\Fixtures\PHP; +use PHPUnit\Framework\TestCase; + $a = new class { public function foo() { @@ -56,4 +58,6 @@ public function fuu() function deng() { -} \ No newline at end of file +} + +$foo = TestCase::class;