Skip to content

Commit

Permalink
fix: handle namespace for Closure without class scope
Browse files Browse the repository at this point in the history
  • Loading branch information
romm committed Nov 4, 2024
1 parent 950395e commit 5affef4
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 16 deletions.
10 changes: 8 additions & 2 deletions src/Type/Parser/Factory/Specifications/AliasSpecification.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,17 @@ private function resolveNamespaced(string $symbol): string
}
}

if (! $reflection->inNamespace()) {
if ($reflection->inNamespace()) {
$namespace = $reflection->getNamespaceName();
} elseif ($reflection instanceof ReflectionFunction) {
$namespace = PhpParser::parseNamespace($reflection);
}

if (! isset($namespace)) {
return $symbol;
}

$full = $reflection->getNamespaceName() . '\\' . $symbol;
$full = "$namespace\\$symbol";

if (Reflection::classOrInterfaceExists($full)) {
return $full;
Expand Down
34 changes: 34 additions & 0 deletions src/Utility/Reflection/NamespaceFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Utility\Reflection;

use PhpToken;

/** @internal */
final class NamespaceFinder
{
public function findNamespace(string $content): ?string
{
$tokens = PhpToken::tokenize($content);
$tokensCount = count($tokens);
/* @infection-ignore-all Unneeded because of the nature of namespace-related token */
$pointer = $tokensCount - 1;

while (! $tokens[$pointer]->is(T_NAMESPACE)) {
/* @infection-ignore-all Unneeded because of the nature of namespace-related token */
if ($pointer === 0) {
return null;
}

$pointer--;
}

while (! $tokens[$pointer]->is([T_NAME_QUALIFIED, T_STRING])) {
$pointer++;
}

return (string)$tokens[$pointer];
}
}
45 changes: 31 additions & 14 deletions src/Utility/Reflection/PhpParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,31 @@ final class PhpParser
* @param ReflectionClass<object>|ReflectionFunction|ReflectionMethod $reflection
* @return array<string, string>
*/
public static function parseUseStatements(\ReflectionClass|\ReflectionFunction|\ReflectionMethod $reflection): array
public static function parseUseStatements(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): array
{
$signature = "{$reflection->getFileName()}:{$reflection->getStartLine()}";

// @infection-ignore-all
return self::$statements[$signature] ??= self::fetchUseStatements($reflection);
}

public static function parseNamespace(ReflectionFunction $reflection): ?string
{
$content = self::getFileContent($reflection);

if ($content === null) {
return null;
}

return (new NamespaceFinder())->findNamespace($content);
}

/**
* @param ReflectionClass<object>|ReflectionFunction|ReflectionMethod $reflection
* @return array<string, string>
*/
private static function fetchUseStatements(\ReflectionClass|\ReflectionFunction|\ReflectionMethod $reflection): array
private static function fetchUseStatements(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): array
{
$filename = $reflection->getFileName();
$startLine = $reflection->getStartLine();

if ($reflection instanceof ReflectionMethod) {
$namespaceName = $reflection->getDeclaringClass()->getNamespaceName();
} elseif ($reflection instanceof ReflectionFunction && $reflection->getClosureScopeClass()) {
Expand All @@ -49,29 +57,38 @@ private static function fetchUseStatements(\ReflectionClass|\ReflectionFunction|
$namespaceName = $reflection->getNamespaceName();
}

// @infection-ignore-all these values will never be `true`
if ($filename === false || $startLine === false) {
return [];
}
$content = self::getFileContent($reflection);

if (! is_file($filename)) {
if ($content === null) {
return [];
}

$content = self::getFileContent($filename, $startLine);

return (new TokenParser($content))->parseUseStatements($namespaceName);
}

private static function getFileContent(string $filename, int $lineNumber): string
/**
* @param ReflectionClass<object>|ReflectionFunction|ReflectionMethod $reflection
*/ private static function getFileContent(ReflectionClass|ReflectionFunction|ReflectionMethod $reflection): ?string
{
$filename = $reflection->getFileName();
$startLine = $reflection->getStartLine();

// @infection-ignore-all these values will never be `true`
if ($filename === false || $startLine === false) {
return null;
}

if (! is_file($filename)) {
return null;
}

// @infection-ignore-all no need to test with `-1`
$lineCnt = 0;
$content = '';
$file = new SplFileObject($filename);

while (! $file->eof()) {
if ($lineCnt++ === $lineNumber) {
if ($lineCnt++ === $startLine) {
break;
}

Expand Down
34 changes: 34 additions & 0 deletions tests/Functional/Utility/Reflection/PhpParserTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace CuyZ\Valinor\Tests\Functional\Utility\Reflection;

use CuyZ\Valinor\Utility\Reflection\PhpParser;
use PHPUnit\Framework\TestCase;
use ReflectionFunction;

final class PhpParserTest extends TestCase
{
public function test_can_parse_namespace_for_closure_with_one_level_namespace(): void
{
$function = require_once 'closure-with-one-level-namespace.php';

$reflection = new ReflectionFunction($function);

$namespace = PhpParser::parseNamespace($reflection);

self::assertSame('OneLevelNamespace', $namespace);
}

public function test_can_parse_namespace_for_closure_with_qualified_namespace(): void
{
$function = require_once 'closure-with-qualified-namespace.php';

$reflection = new ReflectionFunction($function);

$namespace = PhpParser::parseNamespace($reflection);

self::assertSame('Root\QualifiedNamespace', $namespace);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace OneLevelNamespace;

return function () {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace Root\QualifiedNamespace;

return function () {};

0 comments on commit 5affef4

Please sign in to comment.