Skip to content

Commit

Permalink
Refactor class name extraction logic with helper methods
Browse files Browse the repository at this point in the history
Simplify the class name extraction by introducing helper methods for token parsing. This improves readability and maintainability of the code. Added Psalm type annotations for better static analysis.
  • Loading branch information
koriym committed Nov 4, 2024
1 parent d12eac1 commit 01a867b
Showing 1 changed file with 113 additions and 67 deletions.
180 changes: 113 additions & 67 deletions src/ClassName.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Ray\Aop;

use function assert;

Check failure on line 7 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / cs / Coding Standards

Type assert is not used in this file.
use function count;
use function file_exists;
use function file_get_contents;
Expand All @@ -24,6 +25,10 @@
/**
* Extracting the fully qualified class name from a given PHP file
*
* @psalm-type TokenValue = array{0: int, 1: string, 2: int}
* @psalm-type Token = TokenValue|string
* @psalm-type Tokens = array<int, Token>
* @psalm-type ParserResult = array{string, int}
* @psalm-immutable
*/
final class ClassName
Expand All @@ -32,97 +37,138 @@ final class ClassName

/**
* Extract fully qualified class name from file
*
* @psalm-return string|null
*/
public static function from(string $filePath): ?string
{
if (! file_exists($filePath)) {
return null;
}

$namespace = '';
$className = null;
/** @var array<int, array{0: int, 1: string, 2: int}|string> $tokens */
$tokens = token_get_all(file_get_contents($filePath)); // @phpstan-ignore-line
$count = count($tokens);
$position = 0;
$namespace = '';

$i = 0;
while ($i < $count) {
/** @var array{0: int, 1: string, 2: int}|string $token */
$token = $tokens[$i];

if (is_array($token) && $token[0] === T_NAMESPACE) {
$i++;
while ($i < $count && is_array($tokens[$i]) && $tokens[$i][0] === T_WHITESPACE) {
$i++;
}
while ($position < $count) {
[$token, $position] = self::nextToken($tokens, $position);
if (! is_array($token)) {
continue;
}

/** @var array{0: int, 1: string, 2: int}|string $token */
$token = $tokens[$i];
if (is_array($token)) {
if ($token[0] === self::T_NAME_QUALIFIED) {
$namespace = $token[1];
} elseif ($token[0] === T_STRING) {
/** @var list<string> $namespaceParts */
$namespaceParts = [];
while ($i < $count) {
/** @var array{0: int, 1: string, 2: int}|string $token */
$token = $tokens[$i];
if (is_array($token) && $token[0] === T_STRING) {
$namespaceParts[] = $token[1];
$i++;
if ($i < $count && $tokens[$i] === '\\') {
$namespaceParts[] = '\\';
$i++;
}

continue;
}

if ($token === ';' || $token === '{') {
break;
}

$i++;
}

$namespace = implode('', $namespaceParts);
switch ($token[0]) {
case T_NAMESPACE:
[$namespace, $position] = self::parseNamespace($tokens, $position + 1, $count);
continue 2;
case T_CLASS:
$className = self::parseClassName($tokens, $position + 1, $count);
if ($className !== null) {
/** @var string $namespace */
return $namespace !== '' ? $namespace . '\\' . $className : $className;
}
}
}

return null;
}

/**
* @param Tokens $tokens
*
* @return array{Token, int}
*/
private static function nextToken(array $tokens, int $position): array
{
if (is_array($tokens[$position]) && in_array($tokens[$position][0], [T_ABSTRACT, T_FINAL], true)) {
$position++;
}

return [$tokens[$position], $position + 1];
}

/** @param Tokens $tokens */
private static function parseClassName(array $tokens, int $position, int $count): ?string
{
$position = self::skipWhitespace($tokens, $position, $count);
if ($position >= $count || ! is_array($tokens[$position])) {
return null;
}

return $tokens[$position][1];
}

/** @param Tokens $tokens */
private static function skipWhitespace(array $tokens, int $position, int $count): int
{
while (
$position < $count &&
is_array($tokens[$position]) &&
in_array($tokens[$position][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)
) {
$position++;
}

return $position;
}

/**
* @param Tokens $tokens
*
* @return ParserResult
*/
private static function parseNamespace(array $tokens, int $position, int $count): array
{
$position = self::skipWhitespace($tokens, $position, $count);
if (! is_array($tokens[$position])) {
return ['', $position];

Check warning on line 122 in src/ClassName.php

View check run for this annotation

Codecov / codecov/patch

src/ClassName.php#L122

Added line #L122 was not covered by tests
}

if ($tokens[$position][0] === self::T_NAME_QUALIFIED) {
return [$tokens[$position][1], $position + 1];

Check warning on line 126 in src/ClassName.php

View check run for this annotation

Codecov / codecov/patch

src/ClassName.php#L126

Added line #L126 was not covered by tests
}

return $tokens[$position][0] === T_STRING
? self::parseNamespaceParts($tokens, $position, $count)
: ['', $position];
}

/**
* @param Tokens $tokens
*
* @return ParserResult
*/
private static function parseNamespaceParts(array $tokens, int $position, int $count): array
{
/** @var list<string> $namespaceParts */
$namespaceParts = [];

while ($position < $count) {
$token = $tokens[$position];

if (! is_array($token)) {
if ($token === ';' || $token === '{') {
break;
}

$position++;
continue;

Check warning on line 153 in src/ClassName.php

View check run for this annotation

Codecov / codecov/patch

src/ClassName.php#L152-L153

Added lines #L152 - L153 were not covered by tests
}

if (is_array($token) && in_array($token[0], [T_ABSTRACT, T_FINAL], true)) {
$i++;
if ($token[0] !== T_STRING) {
$position++;
continue;
}

if (is_array($token) && $token[0] === T_CLASS) {
$i++;
while (
$i < $count &&
is_array($tokens[$i]) &&
in_array($tokens[$i][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)
) {
$i++;
}
$namespaceParts[] = $token[1];
$position++;

if ($i < $count && is_array($tokens[$i])) {
$className = $tokens[$i][1];
break;
}
if ($position >= $count || $tokens[$position] !== '\\') {
continue;
}

$i++;
}

if ($className === null) {
return null;
$namespaceParts[] = '\\';
$position++;

Check warning on line 169 in src/ClassName.php

View check run for this annotation

Codecov / codecov/patch

src/ClassName.php#L168-L169

Added lines #L168 - L169 were not covered by tests
}

/** @var string $namespace */
return $namespace !== '' ? $namespace . '\\' . $className : $className;
return [implode('', $namespaceParts), $position];
}
}

0 comments on commit 01a867b

Please sign in to comment.