1
0
mirror of https://github.com/danog/PHP-Parser.git synced 2025-01-22 05:41:23 +01:00

Add getShortName() API

PHP's name resolution rules are f'ing complicated.
This commit is contained in:
Nikita Popov 2017-04-28 17:10:30 +02:00
parent 6168abd9a0
commit 56b810e91d
6 changed files with 216 additions and 24 deletions

View File

@ -13,6 +13,9 @@ class NameContext {
/** @var array Map of format [aliasType => [aliasName => originalName]] */
protected $aliases = [];
/** @var array Same as $aliases but preserving original case */
protected $origAliases = [];
/** @var ErrorHandler Error handler */
protected $errorHandler;
@ -34,7 +37,7 @@ class NameContext {
*/
public function startNamespace(Name $namespace = null) {
$this->namespace = $namespace;
$this->aliases = [
$this->origAliases = $this->aliases = [
Stmt\Use_::TYPE_NORMAL => [],
Stmt\Use_::TYPE_FUNCTION => [],
Stmt\Use_::TYPE_CONSTANT => [],
@ -75,6 +78,7 @@ class NameContext {
}
$this->aliases[$type][$aliasLookupName] = $name;
$this->origAliases[$type][$aliasName] = $name;
}
/**
@ -110,11 +114,9 @@ class NameContext {
return $name;
}
$aliasName = strtolower($name->getFirst());
if (!$name->isRelative() && isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName])) {
// resolve aliases (for non-relative names)
$alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName];
return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes());
// Try to resolve aliases
if (null !== $resolvedName = $this->resolveAlias($name, Stmt\Use_::TYPE_NORMAL)) {
return $resolvedName;
}
// if no alias exists prepend current namespace
@ -135,24 +137,12 @@ class NameContext {
return $name;
}
// resolve aliases for qualified names
$aliasName = strtolower($name->getFirst());
if ($name->isQualified() && isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName])) {
$alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$aliasName];
return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes());
// Try to resolve aliases
if (null !== $resolvedName = $this->resolveAlias($name, $type)) {
return $resolvedName;
}
if ($name->isUnqualified()) {
if ($type === Stmt\Use_::TYPE_CONSTANT) {
// constant aliases are case-sensitive, function aliases case-insensitive
$aliasName = $name->getFirst();
}
if (isset($this->aliases[$type][$aliasName])) {
// resolve unqualified aliases
return new FullyQualified($this->aliases[$type][$aliasName], $name->getAttributes());
}
if (null === $this->namespace) {
// outside of a namespace unaliased unqualified is same as fully qualified
return new FullyQualified($name, $name->getAttributes());
@ -165,4 +155,126 @@ class NameContext {
// if no alias exists prepend current namespace
return FullyQualified::concat($this->namespace, $name, $name->getAttributes());
}
/**
* Get possible ways of writing a fully qualified name (e.g., by making use of aliases)
*
* @param FullyQualified $name Fully-qualified name
* @param int $type One of Stmt\Use_::TYPE_*
*
* @return Name[] Possible representations of the name
*/
public function getPossibleNames(FullyQualified $name, $type) {
$nameStr = (string) $name;
$lcName = strtolower($name);
// Collect possible ways to write this name, starting with the fully-qualified name
$possibleNames = [$name];
if (null !== $nsRelativeName = $this->getNamespaceRelativeName($name, $nameStr, $lcName)) {
// Make sure there is no alias that makes the normally namespace-relative name
// into something else
if (null === $this->resolveAlias($nsRelativeName, $type)) {
$possibleNames[] = $nsRelativeName;
}
}
// Check for relevant namespace use statements
foreach ($this->origAliases[Stmt\Use_::TYPE_NORMAL] as $alias => $orig) {
$lcOrig = strtolower((string) $orig);
if (0 === strpos($lcName, $lcOrig . '\\')) {
$possibleNames[] = new Name($alias . substr($name, strlen($lcOrig)));
}
}
// Check for relevant type-specific use statements
foreach ($this->origAliases[$type] as $alias => $orig) {
if ($type === Stmt\Use_::TYPE_CONSTANT) {
// Constants are are complicated-sensitive
if ($this->normalizeConstName($orig) === $this->normalizeConstName($nameStr)) {
$possibleNames[] = new Name($alias);
}
} else {
// Everything else is case-insensitive
if (strtolower((string) $orig) === $lcName) {
$possibleNames[] = new Name($alias);
}
}
}
return $possibleNames;
}
/**
* Get shortest representation of this fully-qualified name.
*
* @param FullyQualified $name Fully-qualified name to shorten
* @param int $type One of Stmt\Use_::TYPE_*
*
* @return Name Shortest representation
*/
public function getShortName(Name\FullyQualified $name, $type) {
$possibleNames = $this->getPossibleNames($name, $type);
// Find shortest name
$shortestName = null;
$shortestLength = INF;
foreach ($possibleNames as $possibleName) {
$length = strlen($possibleName->toCodeString());
if ($length < $shortestLength) {
$shortestName = $possibleName;
$shortestLength = $length;
}
}
return $shortestName;
}
private function resolveAlias(Name $name, $type) {
$firstPart = $name->getFirst();
if ($name->isQualified()) {
// resolve aliases for qualified names, always against class alias table
$checkName = strtolower($firstPart);
if (isset($this->aliases[Stmt\Use_::TYPE_NORMAL][$checkName])) {
$alias = $this->aliases[Stmt\Use_::TYPE_NORMAL][$checkName];
return FullyQualified::concat($alias, $name->slice(1), $name->getAttributes());
}
} elseif ($name->isUnqualified()) {
// constant aliases are case-sensitive, function aliases case-insensitive
$checkName = $type === Stmt\Use_::TYPE_CONSTANT ? $firstPart : strtolower($firstPart);
if (isset($this->aliases[$type][$checkName])) {
// resolve unqualified aliases
return new FullyQualified($this->aliases[$type][$checkName], $name->getAttributes());
}
}
// No applicable aliases
return null;
}
private function getNamespaceRelativeName(Name\FullyQualified $name, $nameStr, $lcName) {
if (null === $this->namespace) {
return new Name($name);
}
$namespacePrefix = strtolower($this->namespace . '\\');
if (0 === strpos($lcName, $namespacePrefix)) {
return new Name(substr($nameStr, strlen($namespacePrefix)));
}
return null;
}
private function normalizeConstName($name) {
$nsSep = strrpos($name, '\\');
if (false === $nsSep) {
return $name;
}
// Constants have case-insensitive namespace and case-sensitive short-name
$ns = substr($name, 0, $nsSep);
$shortName = substr($name, $nsSep + 1);
return strtolower($ns) . '\\' . $shortName;
}
}

View File

@ -82,8 +82,8 @@ class Name extends NodeAbstract
}
/**
* Returns a string representation of the name by imploding the namespace parts with the
* namespace separator.
* Returns a string representation of the name itself, without taking taking the name type into
* account (e.g., not including a leading backslash for fully qualified names).
*
* @return string String representation
*/
@ -91,6 +91,16 @@ class Name extends NodeAbstract
return implode('\\', $this->parts);
}
/**
* Returns a string representation of the name as it would occur in code (e.g., including
* leading backslash for fully qualified names.
*
* @return string String representation
*/
public function toCodeString() {
return $this->toString();
}
/**
* Returns a string representation of the name by imploding the namespace parts with the
* namespace separator.

View File

@ -39,4 +39,8 @@ class FullyQualified extends \PhpParser\Node\Name
public function isRelative() {
return false;
}
public function toCodeString() {
return '\\' . $this->toString();
}
}

View File

@ -39,4 +39,8 @@ class Relative extends \PhpParser\Node\Name
public function isRelative() {
return true;
}
public function toCodeString() {
return 'namespace\\' . $this->toString();
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace PhpParser;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Use_;
use PHPUnit\Framework\TestCase;
class NameContextTest extends TestCase {
/**
* @dataProvider provideTestGetPossibleNames
*/
public function testGetPossibleNames($type, $name, $expectedPossibleNames) {
$nameContext = new NameContext(new ErrorHandler\Throwing());
$nameContext->startNamespace(new Name('NS'));
$nameContext->addAlias(new Name('Foo'), 'Foo', Use_::TYPE_NORMAL);
$nameContext->addAlias(new Name('Foo\Bar'), 'Alias', Use_::TYPE_NORMAL);
$nameContext->addAlias(new Name('Foo\fn'), 'fn', Use_::TYPE_FUNCTION);
$nameContext->addAlias(new Name('Foo\CN'), 'CN', Use_::TYPE_CONSTANT);
$fqName = new Name\FullyQualified($name);
$possibleNames = $nameContext->getPossibleNames($fqName, $type);
$possibleNames = array_map(function (Name $name) {
return $name->toCodeString();
}, $possibleNames);
$this->assertSame($expectedPossibleNames, $possibleNames);
// Here the last name is always the shortest one
$expectedShortName = $expectedPossibleNames[count($expectedPossibleNames) - 1];
$this->assertSame(
$expectedShortName,
$nameContext->getShortName($fqName, $type)->toCodeString()
);
}
public function provideTestGetPossibleNames() {
return [
[Use_::TYPE_NORMAL, 'Test', ['\Test']],
[Use_::TYPE_NORMAL, 'Test\Namespaced', ['\Test\Namespaced']],
[Use_::TYPE_NORMAL, 'NS\Test', ['\NS\Test', 'Test']],
[Use_::TYPE_NORMAL, 'ns\Test', ['\ns\Test', 'Test']],
[Use_::TYPE_NORMAL, 'NS\Foo\Bar', ['\NS\Foo\Bar']],
[Use_::TYPE_NORMAL, 'ns\foo\Bar', ['\ns\foo\Bar']],
[Use_::TYPE_NORMAL, 'Foo', ['\Foo', 'Foo']],
[Use_::TYPE_NORMAL, 'Foo\Bar', ['\Foo\Bar', 'Foo\Bar', 'Alias']],
[Use_::TYPE_NORMAL, 'Foo\Bar\Baz', ['\Foo\Bar\Baz', 'Foo\Bar\Baz', 'Alias\Baz']],
[Use_::TYPE_NORMAL, 'Foo\fn\Bar', ['\Foo\fn\Bar', 'Foo\fn\Bar']],
[Use_::TYPE_FUNCTION, 'Foo\fn\bar', ['\Foo\fn\bar', 'Foo\fn\bar']],
[Use_::TYPE_FUNCTION, 'Foo\fn', ['\Foo\fn', 'Foo\fn', 'fn']],
[Use_::TYPE_FUNCTION, 'Foo\FN', ['\Foo\FN', 'Foo\FN', 'fn']],
[Use_::TYPE_CONSTANT, 'Foo\CN\BAR', ['\Foo\CN\BAR', 'Foo\CN\BAR']],
[Use_::TYPE_CONSTANT, 'Foo\CN', ['\Foo\CN', 'Foo\CN', 'CN']],
[Use_::TYPE_CONSTANT, 'foo\CN', ['\foo\CN', 'Foo\CN', 'CN']],
[Use_::TYPE_CONSTANT, 'foo\cn', ['\foo\cn', 'Foo\cn']],
];
}
}

View File

@ -100,30 +100,34 @@ class NameTest extends TestCase
$this->assertNull(Name::concat(null, null));
}
public function testIs() {
public function testNameTypes() {
$name = new Name('foo');
$this->assertTrue ($name->isUnqualified());
$this->assertFalse($name->isQualified());
$this->assertFalse($name->isFullyQualified());
$this->assertFalse($name->isRelative());
$this->assertSame('foo', $name->toCodeString());
$name = new Name('foo\bar');
$this->assertFalse($name->isUnqualified());
$this->assertTrue ($name->isQualified());
$this->assertFalse($name->isFullyQualified());
$this->assertFalse($name->isRelative());
$this->assertSame('foo\bar', $name->toCodeString());
$name = new Name\FullyQualified('foo');
$this->assertFalse($name->isUnqualified());
$this->assertFalse($name->isQualified());
$this->assertTrue ($name->isFullyQualified());
$this->assertFalse($name->isRelative());
$this->assertSame('\foo', $name->toCodeString());
$name = new Name\Relative('foo');
$this->assertFalse($name->isUnqualified());
$this->assertFalse($name->isQualified());
$this->assertFalse($name->isFullyQualified());
$this->assertTrue ($name->isRelative());
$this->assertSame('namespace\foo', $name->toCodeString());
}
/**