src/Debug/Utility/UseStatements.php
<?php
/**
* This file is part of PHPDebugConsole
*
* @package PHPDebugConsole
* @author Brad Kent <bkfake-github@yahoo.com>
* @license http://opensource.org/licenses/MIT MIT
* @copyright 2014-2024 Brad Kent
* @since 2.3
*/
namespace bdk\Debug\Utility;
use ReflectionClass;
/**
* Find use statements for given class
*
* @see https://www.php.net/manual/en/language.namespaces.importing.php
* @see https://www.php.net/manual/en/language.namespaces.definitionmultiple.php
*
* @psalm-type useStatements = array<'class'|'function'|'const', array<string, string>>
*/
class UseStatements
{
/** @var array<string,useStatements> */
protected static $cache = array();
/** @var array{class:string,alias:string}|null */
protected static $currentUse = null;
/** @var array<'class'|'function'|'const',array<empty,empty>> */
protected static $categories = array(
'class' => array(),
'const' => array(),
'function' => array(),
);
/**
* Maintain "group" namespace (PHP 7+)
* ie `use function some\namespace\{fn_a, fn_b, fn_c};`
*
* @var string|null
*/
protected static $groupNamespace = null;
/** @var string|null */
protected static $namespace = null;
/** @var string|null */
protected static $record = null; // 'class', 'const', 'function', or 'namespace'
/** @var string|null */
protected static $recordPart = null; // 'alias' or 'class'
/** @var useStatements */
protected static $useStatements = array();
/**
* Return array of use statements from class.
*
* @param ReflectionClass $reflector ReflectionClass instance
*
* @return useStatements
*/
public static function getUseStatements(ReflectionClass $reflector)
{
if (!$reflector->isUserDefined()) {
return self::$categories;
}
$name = $reflector->getName();
if (isset(self::$cache[$name])) {
return self::$cache[$name];
}
$source = self::getPrecedingLines($reflector->getFileName(), $reflector->getStartLine());
$useStatements = \strpos($source, 'use')
? self::extractUse($source)
: array();
$namespace = $reflector->getNamespaceName();
$useStatements = isset($useStatements[$namespace])
? $useStatements[$namespace]
: self::$categories;
self::$cache[$name] = $useStatements;
return $useStatements;
}
/**
* Parse the use statements from given source code
*
* @param string $source php code
*
* @return array<string,useStatements> Use statements grouped by namespace
*/
public static function extractUse($source)
{
$tokens = \token_get_all($source);
self::$namespace = null;
self::$currentUse = null;
self::$groupNamespace = null;
self::$record = null;
self::$recordPart = null;
self::$useStatements = array();
if (\defined('T_NAME_QUALIFIED') === false) {
// T_NAME_QUALIFIED introduced with PHP 8.0
\define('T_NAME_QUALIFIED', 314);
}
foreach ($tokens as $token) {
if (self::$record) {
self::recordToken($token);
continue;
}
// check if we need to start recording
self::setRecordInfo($token);
}
return self::sort(self::$useStatements);
}
/**
* Add currentUse to self::$useStatements -> namespace
*
* @return void
*/
private static function addUseStatement()
{
$class = \ltrim(self::$currentUse['class'], '\\');
$alias = self::$currentUse['alias'] ?: self::getShortName($class);
if (!isset(self::$useStatements[self::$namespace])) {
self::$useStatements[self::$namespace] = self::$categories;
}
self::$useStatements[self::$namespace][self::$record][$alias] = $class;
self::$currentUse = null;
}
/**
* Read file source up to the line where our class is defined.
*
* @param string $file filepath
* @param int $startLine line to stop reading source
*
* @return string
*/
private static function getPrecedingLines($file, $startLine)
{
$file = \fopen($file, 'r');
$line = 0;
$source = '';
while (\feof($file) === false) {
++$line;
if ($line >= $startLine) {
break;
}
$source .= \fgets($file);
}
\fclose($file);
return $source;
}
/**
* Get classname's "short name" (sans namespace)
*
* @param string $classname classname
*
* @return string
*/
private static function getShortName($classname)
{
$pos = \strrpos($classname, '\\');
return $pos
? \substr($classname, $pos + 1)
: $classname;
}
/**
* Record the specified token
*
* @param array|string $token Token to record
*
* @return void
*/
private static function recordToken($token)
{
if (\is_array($token) === false) {
self::recordTokenString($token);
return;
}
self::setRecordInfo($token);
self::recordTokenArray($token);
}
/**
* Record the specified array token
*
* @param array $token Token to record
*
* @return void
*/
private static function recordTokenArray($token) // phpcs:ignore Generic.Metrics.CyclomaticComplexity
{
switch (self::$record) {
case 'namespace':
switch ($token[0]) {
case T_STRING:
case T_NS_SEPARATOR:
case T_NAME_QUALIFIED:
self::$namespace .= $token[1];
break;
}
break;
case 'class':
case 'function':
case 'const':
switch ($token[0]) {
case T_STRING:
case T_NS_SEPARATOR:
case T_NAME_QUALIFIED:
self::$currentUse[self::$recordPart] .= $token[1];
break;
}
break;
}
}
/**
* comma encountered... reset currentUse
*
* @return void
*/
private static function recordTokenClass()
{
self::$currentUse = array(
'alias' => '',
'class' => self::$groupNamespace ?: '',
);
self::$recordPart = 'class';
}
/**
* Handle simple string token while recording
*
* @param string $token string token (ie "(",")",",", or ";" )
*
* @return void
*/
private static function recordTokenString($token)
{
if ($token === '{') {
// start group (PHP 7.0+)
self::$groupNamespace = \ltrim(self::$currentUse['class'], '\\');
return;
}
if ($token === '}') {
// end group
self::$groupNamespace = null;
return;
}
if (self::$currentUse) {
self::addUseStatement();
}
if ($token === ',') {
// multiple use statements on the same line
self::recordTokenClass();
return;
}
self::$record = null;
}
/**
* Test if we should start "recording" use-statement info
*
* @param array|string $token token to test
*
* @return void
*/
private static function setRecordInfo($token)
{
// $token may not be an array, but that's ok... $token[0] will just be first char of string
switch ($token[0]) {
case T_AS:
self::$recordPart = 'alias';
break;
case T_CONST:
// PHP 5.6+ `use const My\Full\CONSTANT;`
self::$record = 'const';
break;
case T_FUNCTION:
// PHP 5.6+ `use function My\Full\functionName as func;`
self::$record = 'function';
break;
case T_NAMESPACE:
self::$record = 'namespace';
break;
case T_USE:
self::$record = 'class';
self::$recordPart = 'class';
self::$currentUse = array(
'alias' => '',
'class' => '',
);
break;
}
}
/**
* Sort use statements by namespace & alias
*
* @param array<string,useStatements> $statements use statement grouped by namespace
*
* @return array<string,useStatements>
*/
private static function sort(array $statements)
{
\ksort($statements);
foreach ($statements as &$nsStatements) {
foreach ($nsStatements as &$useStatements) {
\ksort($useStatements);
}
}
return $statements;
}
}