From c6ffb7c9b78a87a254f7555b2429844509401eed Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Mon, 3 Jun 2024 18:18:01 +1200 Subject: [PATCH 1/6] Add php-parser dep and refactor `TokenScanner` to use it --- composer.json | 1 + src/Analysers/TokenScanner.php | 432 +++++++++------------------------ 2 files changed, 112 insertions(+), 321 deletions(-) diff --git a/composer.json b/composer.json index ef43831c..4fc4207d 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,7 @@ "require": { "php": ">=7.2", "ext-json": "*", + "nikic/php-parser": "^4.19", "psr/log": "^1.1 || ^2.0 || ^3.0", "symfony/deprecation-contracts": "^2 || ^3", "symfony/finder": ">=2.2", diff --git a/src/Analysers/TokenScanner.php b/src/Analysers/TokenScanner.php index 872753db..085a67cf 100644 --- a/src/Analysers/TokenScanner.php +++ b/src/Analysers/TokenScanner.php @@ -6,6 +6,20 @@ namespace OpenApi\Analysers; +use PhpParser\Error; +use PhpParser\Node\Name; +use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassLike; +use PhpParser\Node\Stmt\ClassMethod; +use PhpParser\Node\Stmt\Enum_; +use PhpParser\Node\Stmt\Interface_; +use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Property; +use PhpParser\Node\Stmt\Trait_; +use PhpParser\Node\Stmt\TraitUse; +use PhpParser\Node\Stmt\Use_; +use PhpParser\ParserFactory; + /** * High level, PHP token based, scanner. */ @@ -18,364 +32,140 @@ class TokenScanner */ 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> 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; + $parser = (new ParserFactory())->createForNewestSupportedVersion(); + try { + $stmts = $parser->parse(file_get_contents($filename)); + } catch (Error $e) { + throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); + } - 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); - } + $result = []; + foreach ($stmts as $stmt) { + //echo 'top: ' . get_class($stmt), PHP_EOL; + if ($stmt instanceof Namespace_) { + $namespace = (string)$stmt->name; - 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; - } + $uses = []; + $resolve = function(string $name) use ($namespace, &$uses) { + if (array_key_exists($name, $uses)) { + return $uses[$name]; } - 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) { + return $namespace.'\\'.$name; + }; + foreach ($stmt->stmts as $subStmt) { + //echo 'sub: ' . get_class($subStmt), PHP_EOL; + switch (get_class($subStmt)) { + case Use_::class: + $uses += $this->collect_uses($subStmt); + break; + case Class_::class: + $result += $this->collect_class($subStmt, $uses, $resolve); + break; + case Interface_::class: + $result += $this->collect_interface($subStmt, $uses, $resolve); + break; + case Trait_::class: + $result += $this->collect_trait($subStmt, $uses, $resolve); + break; + case Enum_::class: + $result += $this->collect_enum($subStmt, $uses, $resolve); 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; + return $result; } - /** - * Get the next token that is not whitespace or comment. - * - * @return string|array|false - */ - protected function nextToken(array &$tokens) + protected function collect_uses(Use_ $stmt): array { - $token = true; - while ($token) { - $token = next($tokens); - if (is_array($token)) { - if (in_array($token[0], [T_WHITESPACE, T_COMMENT])) { - continue; - } - } + $uses = []; - return $token; + foreach ($stmt->uses as $use) { + $uses[(string)$use->getAlias()] = (string)$use->name; } - return $token; + return $uses; } - /** - * @return array - */ - protected function resolveFQN(array $names, string $namespace, array $uses): array + protected function collect_classlike(ClassLike $stmt, array $details, callable $resolve): + 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)); + if (!array_key_exists('properties', $details)) { + $details['properties'] = []; + } + $details['properties'] = array_merge(array_map(function (Property $p) { + return (string)$p->props[0]->name; + }, $stmt->getProperties()), $details['properties']); + $details['methods'] = array_map(function (ClassMethod $m) { + return (string)$m->name; + }, $stmt->getMethods()); + $details['traits'] = array_map(function (TraitUse $traitUse) use ($resolve) { + return $resolve((string)$traitUse->traits[0]); + }, $stmt->getTraitUses()); + + return [ + $resolve($stmt->name->name) => $details, + ]; } - protected function skipTo(array &$tokens, string $char, bool $prev = false): void + protected function collect_class(Class_ $stmt, array $uses, callable $resolve): array { - while (false !== ($token = next($tokens))) { - if (is_string($token) && $token == $char) { - if ($prev) { - prev($tokens); + $details = []; + + $details['uses'] = $uses; + $details['interfaces'] = array_map(function (Name $name) use ($resolve) { + return $resolve((string)$name); + }, $stmt->implements); + $details['enums'] = []; + + // promoted properties + if ($ctor = $stmt->getMethod('__construct')) { + foreach ($ctor->getParams() as $param) { + if ($param->flags) { + $details['properties'][] = $param->var->name; } - - break; } } + + return $this->collect_classlike($stmt, $details, $resolve); } - /** - * Read next word. - * - * Skips leading whitespace. - */ - protected function nextWord(array &$tokens): string + protected function collect_interface(Interface_ $stmt, array $uses, callable $resolve): array { - $word = ''; - while (false !== ($token = next($tokens))) { - if (is_array($token)) { - if ($token[0] === T_WHITESPACE) { - if ($word) { - break; - } - continue; - } - $word .= $token[1]; - } - } + $details = []; - return $word; + $details['uses'] = $uses; + $details['interfaces'] = array_map(function (Name $name) use ($resolve) { + return $resolve((string)$name); + }, $stmt->extends); + $details['enums'] = []; + + return $this->collect_classlike($stmt, $details, $resolve); } - /** - * Parse a use statement. - */ - protected function parseFQNStatement(array &$tokens, array &$token): array + protected function collect_trait(Trait_ $stmt, array $uses, callable $resolve): array { - $normalizeAlias = function ($alias): string { - $alias = ltrim($alias, '\\'); - $elements = explode('\\', $alias); + $details = []; - return array_pop($elements); - }; + $details['uses'] = $uses; + $details['interfaces'] = []; + $details['enums'] = []; - $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; + return $this->collect_classlike($stmt, $details, $resolve); } - protected function parsePromotedProperties(array &$tokens): array + protected function collect_enum(Enum_ $stmt, array $uses, callable $resolve): array { - $properties = []; + $details = []; - $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; - } - } - } + $details['uses'] = $uses; + $details['interfaces'] = []; + $details['traits'] = []; + $details['enums'] = []; - return $properties; + return $this->collect_classlike($stmt, $details, $resolve); } } From 6f2ca2c4b4c7b3ea200764a9dfd2e8288f9b6643 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Mon, 3 Jun 2024 18:19:50 +1200 Subject: [PATCH 2/6] Update phpstan baseline --- phpstan-baseline.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 3e34c3bc..887e4527 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -31,7 +31,7 @@ parameters: path: Examples/using-links-php81/User.php - - message: "#^Strict comparison using \\=\\=\\= between array\\|string and false will always evaluate to false\\.$#" + message: "#^Call to function array_key_exists\\(\\) with string and array\\{\\} will always evaluate to false\\.$#" count: 1 path: src/Analysers/TokenScanner.php From 090221020034430d8fb66d0b35479fa203fbb2ef Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Mon, 3 Jun 2024 19:50:15 +1200 Subject: [PATCH 3/6] Tweak implementation / tests --- src/Analysers/TokenScanner.php | 109 +++++++++++---------------- tests/Analysers/TokenScannerTest.php | 26 ++++++- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/src/Analysers/TokenScanner.php b/src/Analysers/TokenScanner.php index 085a67cf..abfa38af 100644 --- a/src/Analysers/TokenScanner.php +++ b/src/Analysers/TokenScanner.php @@ -7,16 +7,12 @@ namespace OpenApi\Analysers; use PhpParser\Error; -use PhpParser\Node\Name; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassLike; -use PhpParser\Node\Stmt\ClassMethod; use PhpParser\Node\Stmt\Enum_; use PhpParser\Node\Stmt\Interface_; use PhpParser\Node\Stmt\Namespace_; -use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Trait_; -use PhpParser\Node\Stmt\TraitUse; use PhpParser\Node\Stmt\Use_; use PhpParser\ParserFactory; @@ -41,35 +37,45 @@ public function scanFile(string $filename): array $result = []; foreach ($stmts as $stmt) { - //echo 'top: ' . get_class($stmt), PHP_EOL; + // echo 'top: ' . get_class($stmt), PHP_EOL; if ($stmt instanceof Namespace_) { - $namespace = (string)$stmt->name; + $namespace = (string) $stmt->name; $uses = []; - $resolve = function(string $name) use ($namespace, &$uses) { + $resolve = function (string $name) use ($namespace, &$uses) { if (array_key_exists($name, $uses)) { return $uses[$name]; } - return $namespace.'\\'.$name; + return $namespace . '\\' . $name; + }; + $details = function () use (&$uses) { + return [ + 'uses' => $uses, + 'interfaces' => [], + 'traits' => [], + 'enums' => [], + 'methods' => [], + 'properties' => [], + ]; }; foreach ($stmt->stmts as $subStmt) { - //echo 'sub: ' . get_class($subStmt), PHP_EOL; + // echo 'sub: ' . get_class($subStmt), PHP_EOL; switch (get_class($subStmt)) { case Use_::class: $uses += $this->collect_uses($subStmt); break; case Class_::class: - $result += $this->collect_class($subStmt, $uses, $resolve); + $result += $this->collect_class($subStmt, $details(), $resolve); break; case Interface_::class: - $result += $this->collect_interface($subStmt, $uses, $resolve); + $result += $this->collect_interface($subStmt, $details(), $resolve); break; case Trait_::class: - $result += $this->collect_trait($subStmt, $uses, $resolve); + $result += $this->collect_classlike($subStmt, $details(), $resolve); break; case Enum_::class: - $result += $this->collect_enum($subStmt, $uses, $resolve); + $result += $this->collect_classlike($subStmt, $details(), $resolve); break; } } @@ -84,42 +90,40 @@ protected function collect_uses(Use_ $stmt): array $uses = []; foreach ($stmt->uses as $use) { - $uses[(string)$use->getAlias()] = (string)$use->name; + $uses[(string) $use->getAlias()] = (string) $use->name; } return $uses; } - protected function collect_classlike(ClassLike $stmt, array $details, callable $resolve): - array + protected function collect_classlike(ClassLike $stmt, array $details, callable $resolve): array { - if (!array_key_exists('properties', $details)) { - $details['properties'] = []; + foreach ($stmt->getProperties() as $properties) { + foreach ($properties->props as $prop) { + $details['properties'][] = (string) $prop->name; + } + } + + foreach ($stmt->getMethods() as $method) { + $details['methods'][] = (string) $method->name; + } + + foreach ($stmt->getTraitUses() as $traitUse) { + foreach ($traitUse->traits as $trait) { + $details['traits'][] = $resolve((string) $trait); + } } - $details['properties'] = array_merge(array_map(function (Property $p) { - return (string)$p->props[0]->name; - }, $stmt->getProperties()), $details['properties']); - $details['methods'] = array_map(function (ClassMethod $m) { - return (string)$m->name; - }, $stmt->getMethods()); - $details['traits'] = array_map(function (TraitUse $traitUse) use ($resolve) { - return $resolve((string)$traitUse->traits[0]); - }, $stmt->getTraitUses()); return [ $resolve($stmt->name->name) => $details, ]; } - protected function collect_class(Class_ $stmt, array $uses, callable $resolve): array + protected function collect_class(Class_ $stmt, array $details, callable $resolve): array { - $details = []; - - $details['uses'] = $uses; - $details['interfaces'] = array_map(function (Name $name) use ($resolve) { - return $resolve((string)$name); - }, $stmt->implements); - $details['enums'] = []; + foreach ($stmt->implements as $implement) { + $details['interfaces'][] = $resolve((string) $implement); + } // promoted properties if ($ctor = $stmt->getMethod('__construct')) { @@ -133,38 +137,11 @@ protected function collect_class(Class_ $stmt, array $uses, callable $resolve): return $this->collect_classlike($stmt, $details, $resolve); } - protected function collect_interface(Interface_ $stmt, array $uses, callable $resolve): array + protected function collect_interface(Interface_ $stmt, array $details, callable $resolve): array { - $details = []; - - $details['uses'] = $uses; - $details['interfaces'] = array_map(function (Name $name) use ($resolve) { - return $resolve((string)$name); - }, $stmt->extends); - $details['enums'] = []; - - return $this->collect_classlike($stmt, $details, $resolve); - } - - protected function collect_trait(Trait_ $stmt, array $uses, callable $resolve): array - { - $details = []; - - $details['uses'] = $uses; - $details['interfaces'] = []; - $details['enums'] = []; - - return $this->collect_classlike($stmt, $details, $resolve); - } - - protected function collect_enum(Enum_ $stmt, array $uses, callable $resolve): array - { - $details = []; - - $details['uses'] = $uses; - $details['interfaces'] = []; - $details['traits'] = []; - $details['enums'] = []; + foreach ($stmt->extends as $extend) { + $details['interfaces'][] = $resolve((string) $extend); + } return $this->collect_classlike($stmt, $details, $resolve); } diff --git a/tests/Analysers/TokenScannerTest.php b/tests/Analysers/TokenScannerTest.php index b58feec4..cde7a872 100644 --- a/tests/Analysers/TokenScannerTest.php +++ b/tests/Analysers/TokenScannerTest.php @@ -48,7 +48,7 @@ public static function scanCases(): iterable 'traits' => ['OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\NameTrait'], 'enums' => [], 'methods' => ['__construct'], - 'properties' => ['quantity', 'brand', 'colour', 'id', 'releasedAt'], + 'properties' => ['releasedAt', 'quantity', 'brand', 'colour', 'id'], ], 'OpenApi\\Tests\\Fixtures\\Apis\\DocBlocks\\ProductController' => [ 'uses' => ['OA' => 'OpenApi\\Annotations'], @@ -195,7 +195,10 @@ public static function scanCases(): iterable 'OpenApi\\Tests\\Fixtures\\Parser\\AllTraits' => [ 'uses' => [], 'interfaces' => [], - 'traits' => ['OpenApi\\Tests\\Fixtures\\Parser\\AsTrait', 'OpenApi\\Tests\\Fixtures\\Parser\\HelloTrait'], + 'traits' => [ + 'OpenApi\\Tests\\Fixtures\\Parser\\AsTrait', + 'OpenApi\\Tests\\Fixtures\\Parser\\HelloTrait', + ], 'enums' => [], 'methods' => [], 'properties' => [], @@ -220,6 +223,25 @@ public static function scanCases(): iterable ], ]; + yield 'HelloTrait' => [ + 'Parser/HelloTrait.php', + [ + 'OpenApi\\Tests\\Fixtures\\Parser\\HelloTrait' => [ + 'uses' => [ + 'Aliased' => 'OpenApi\\Tests\\Fixtures\\Parser\\AsTrait', + ], + 'interfaces' => [], + 'traits' => [ + 'OpenApi\\Tests\\Fixtures\\Parser\\OtherTrait', + 'OpenApi\\Tests\\Fixtures\\Parser\\AsTrait', + ], + 'enums' => [], + 'methods' => [], + 'properties' => ['greet'], + ], + ], + ]; + yield 'Php8PromotedProperties' => [ 'PHP/Php8PromotedProperties.php', [ From 4432b4acbf536d22e165272d0d9e43fa179b7d8c Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Mon, 3 Jun 2024 19:55:45 +1200 Subject: [PATCH 4/6] Fix scratch test namespaces --- tests/Fixtures/Scratch/ExclusiveMinMax.php | 2 ++ tests/Fixtures/Scratch/ExclusiveMinMax3.0.0.yaml | 2 +- tests/Fixtures/Scratch/ExclusiveMinMax3.1.0.yaml | 2 +- tests/Fixtures/Scratch/NullRef.php | 2 ++ tests/Fixtures/Scratch/ParameterContent.php | 2 ++ 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Fixtures/Scratch/ExclusiveMinMax.php b/tests/Fixtures/Scratch/ExclusiveMinMax.php index 1cce4dff..d88c5868 100644 --- a/tests/Fixtures/Scratch/ExclusiveMinMax.php +++ b/tests/Fixtures/Scratch/ExclusiveMinMax.php @@ -4,6 +4,8 @@ * @license Apache 2.0 */ +namespace OpenApi\Tests\Fixtures\Scratch; + use OpenApi\Attributes as OAT; #[OAT\Schema(schema: 'minMaxClass')] diff --git a/tests/Fixtures/Scratch/ExclusiveMinMax3.0.0.yaml b/tests/Fixtures/Scratch/ExclusiveMinMax3.0.0.yaml index 0c3e258d..adbe4b0a 100644 --- a/tests/Fixtures/Scratch/ExclusiveMinMax3.0.0.yaml +++ b/tests/Fixtures/Scratch/ExclusiveMinMax3.0.0.yaml @@ -6,7 +6,7 @@ paths: /api/endpoint: get: description: 'An endpoint' - operationId: ef57acec977120506db6b2cf1c500c15 + operationId: 196df2e45989ede264698d505424af54 responses: '200': description: OK diff --git a/tests/Fixtures/Scratch/ExclusiveMinMax3.1.0.yaml b/tests/Fixtures/Scratch/ExclusiveMinMax3.1.0.yaml index fc27050b..c9c61d8a 100644 --- a/tests/Fixtures/Scratch/ExclusiveMinMax3.1.0.yaml +++ b/tests/Fixtures/Scratch/ExclusiveMinMax3.1.0.yaml @@ -6,7 +6,7 @@ paths: /api/endpoint: get: description: 'An endpoint' - operationId: ef57acec977120506db6b2cf1c500c15 + operationId: 196df2e45989ede264698d505424af54 responses: '200': description: OK diff --git a/tests/Fixtures/Scratch/NullRef.php b/tests/Fixtures/Scratch/NullRef.php index b7435318..55b5ab54 100644 --- a/tests/Fixtures/Scratch/NullRef.php +++ b/tests/Fixtures/Scratch/NullRef.php @@ -4,6 +4,8 @@ * @license Apache 2.0 */ +namespace OpenApi\Tests\Fixtures\Scratch; + use OpenApi\Attributes as OAT; #[OAT\Schema(schema: 'repository')] diff --git a/tests/Fixtures/Scratch/ParameterContent.php b/tests/Fixtures/Scratch/ParameterContent.php index 2d388934..82914363 100644 --- a/tests/Fixtures/Scratch/ParameterContent.php +++ b/tests/Fixtures/Scratch/ParameterContent.php @@ -4,6 +4,8 @@ * @license Apache 2.0 */ +namespace OpenApi\Tests\Fixtures\Scratch; + use OpenApi\Attributes as OAT; /** From 3230adc9c46dff9023baaf25649e4d34b840d041 Mon Sep 17 00:00:00 2001 From: DerManoMann Date: Mon, 3 Jun 2024 20:11:44 +1200 Subject: [PATCH 5/6] Handle no namespace --- src/Analysers/TokenScanner.php | 85 +++++++++++++++------------- tests/Analysers/TokenScannerTest.php | 14 +++++ tests/Fixtures/PHP/namespaces3.php | 8 +++ 3 files changed, 68 insertions(+), 39 deletions(-) create mode 100644 tests/Fixtures/PHP/namespaces3.php diff --git a/src/Analysers/TokenScanner.php b/src/Analysers/TokenScanner.php index abfa38af..5ecad309 100644 --- a/src/Analysers/TokenScanner.php +++ b/src/Analysers/TokenScanner.php @@ -36,49 +36,56 @@ public function scanFile(string $filename): array } $result = []; + $result += $this->collect_stmts($stmts, ''); foreach ($stmts as $stmt) { - // echo 'top: ' . get_class($stmt), PHP_EOL; if ($stmt instanceof Namespace_) { $namespace = (string) $stmt->name; - $uses = []; - $resolve = function (string $name) use ($namespace, &$uses) { - if (array_key_exists($name, $uses)) { - return $uses[$name]; - } - - return $namespace . '\\' . $name; - }; - $details = function () use (&$uses) { - return [ - 'uses' => $uses, - 'interfaces' => [], - 'traits' => [], - 'enums' => [], - 'methods' => [], - 'properties' => [], - ]; - }; - foreach ($stmt->stmts as $subStmt) { - // echo 'sub: ' . get_class($subStmt), PHP_EOL; - switch (get_class($subStmt)) { - case Use_::class: - $uses += $this->collect_uses($subStmt); - break; - case Class_::class: - $result += $this->collect_class($subStmt, $details(), $resolve); - break; - case Interface_::class: - $result += $this->collect_interface($subStmt, $details(), $resolve); - break; - case Trait_::class: - $result += $this->collect_classlike($subStmt, $details(), $resolve); - break; - case Enum_::class: - $result += $this->collect_classlike($subStmt, $details(), $resolve); - break; - } - } + $result += $this->collect_stmts($stmt->stmts, $namespace); + } + } + + return $result; + } + + protected function collect_stmts(array $stmts, string $namespace): array + { + $uses = []; + $resolve = function (string $name) use ($namespace, &$uses) { + if (array_key_exists($name, $uses)) { + return $uses[$name]; + } + + return $namespace . '\\' . $name; + }; + $details = function () use (&$uses) { + return [ + 'uses' => $uses, + 'interfaces' => [], + 'traits' => [], + 'enums' => [], + 'methods' => [], + 'properties' => [], + ]; + }; + $result = []; + foreach ($stmts as $stmt) { + switch (get_class($stmt)) { + case Use_::class: + $uses += $this->collect_uses($stmt); + break; + case Class_::class: + $result += $this->collect_class($stmt, $details(), $resolve); + break; + case Interface_::class: + $result += $this->collect_interface($stmt, $details(), $resolve); + break; + case Trait_::class: + $result += $this->collect_classlike($stmt, $details(), $resolve); + break; + case Enum_::class: + $result += $this->collect_classlike($stmt, $details(), $resolve); + break; } } diff --git a/tests/Analysers/TokenScannerTest.php b/tests/Analysers/TokenScannerTest.php index cde7a872..b62b51fa 100644 --- a/tests/Analysers/TokenScannerTest.php +++ b/tests/Analysers/TokenScannerTest.php @@ -382,6 +382,20 @@ public static function scanCases(): iterable ], ]; + yield 'namespaces3' => [ + 'PHP/namespaces3.php', + [ + '\\BarClass' => [ + 'uses' => [], + 'interfaces' => [], + 'traits' => [], + 'enums' => [], + 'methods' => [], + 'properties' => [], + ], + ], + ]; + if (\PHP_VERSION_ID >= 80100) { yield 'enum' => [ 'PHP/Enums/StatusEnum.php', diff --git a/tests/Fixtures/PHP/namespaces3.php b/tests/Fixtures/PHP/namespaces3.php new file mode 100644 index 00000000..b647b797 --- /dev/null +++ b/tests/Fixtures/PHP/namespaces3.php @@ -0,0 +1,8 @@ + Date: Mon, 3 Jun 2024 20:13:35 +1200 Subject: [PATCH 6/6] Fix last scratch now that we have a no namespace fixture --- tests/Fixtures/Scratch/ThirdPartyAnnotation.php | 6 +++++- tests/Fixtures/Scratch/ThirdPartyAnnotation3.0.0.yaml | 2 +- tests/Fixtures/Scratch/ThirdPartyAnnotation3.1.0.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/Fixtures/Scratch/ThirdPartyAnnotation.php b/tests/Fixtures/Scratch/ThirdPartyAnnotation.php index 300cca53..5282ac9f 100644 --- a/tests/Fixtures/Scratch/ThirdPartyAnnotation.php +++ b/tests/Fixtures/Scratch/ThirdPartyAnnotation.php @@ -1,6 +1,10 @@