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 27db0de
Showing 1 changed file with 116 additions and 68 deletions.
184 changes: 116 additions & 68 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;
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,140 @@ 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
$tokens = token_get_all(file_get_contents($filePath));

Check failure on line 47 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / PHPStan

Parameter #1 $code of function token_get_all expects string, string|false given.
assert($tokens instanceof Tokens); // @phpstan-ignore-line

Check failure on line 48 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

TypeDoesNotContainType: Cannot resolve types for $tokens - list<array{0: int, 1: string, 2: int}|string> does not contain Ray\Aop\Tokens

Check failure on line 48 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

MixedArgument: Argument 1 of assert cannot be mixed, expecting bool|int|string

Check failure on line 48 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

UndefinedClass: Class, interface or enum named Ray\Aop\Tokens does not exist
$count = count($tokens);

Check failure on line 49 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

InvalidArgument: Argument 1 of count expects Countable|array<array-key, mixed>, but Ray\Aop\Tokens provided
$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);

Check failure on line 54 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

InvalidArgument: Argument 1 of Ray\Aop\ClassName::nextToken expects array<int, array{0: int, 1: string, 2: int}|string>, but Ray\Aop\Tokens provided
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);

Check failure on line 61 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

InvalidArgument: Argument 1 of Ray\Aop\ClassName::parseNamespace expects array<int, array{0: int, 1: string, 2: int}|string>, but Ray\Aop\Tokens provided
continue 2;
case T_CLASS:
$className = self::parseClassName($tokens, $position + 1, $count);

Check failure on line 64 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

InvalidArgument: Argument 1 of Ray\Aop\ClassName::parseClassName expects array<int, array{0: int, 1: string, 2: int}|string>, but Ray\Aop\Tokens provided
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];
}

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

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];
assert($token instanceof Token);

Check failure on line 147 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / PHPStan

Call to function assert() with false will always evaluate to false.

Check failure on line 147 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / PHPStan

Class Ray\Aop\Token not found.

Check failure on line 147 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / PHPStan

Instanceof between array<int, int|string>|string and Ray\Aop\Token will always evaluate to false.

Check failure on line 147 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

DocblockTypeContradiction: Cannot resolve types for $token - docblock-defined type array{0: int, 1: string, 2: int}|string does not contain Ray\Aop\Token

Check failure on line 147 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

UndefinedClass: Class, interface or enum named Ray\Aop\Token does not exist

if (! is_array($token)) {

Check failure on line 149 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / Psalm

RedundantConditionGivenDocblockType: Docblock-defined type Ray\Aop\Token for $token is never array<array-key, mixed>
if ($token === ';' || $token === '{') {

Check failure on line 150 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / PHPStan

Result of || is always false.

Check failure on line 150 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / PHPStan

Strict comparison using === between *NEVER* and ';' will always evaluate to false.

Check failure on line 150 in src/ClassName.php

View workflow job for this annotation

GitHub Actions / sa / PHPStan

Strict comparison using === between *NEVER* and '{' will always evaluate to false.
break;
}

$position++;
continue;
}

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++;
}

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

0 comments on commit 27db0de

Please sign in to comment.