fix(namespace): isolate explicit import aliases

master
韩天峰 2 days ago
parent af2b03c111
commit 1b92ae6e7b
  1. 11
      phpunit/code/namespace/explicit-alias-original-name.php
  2. 33
      phpunit/src/CompilerBaseApiTest.php
  3. 14
      phpunit/src/NamespaceAliasResolutionTest.php
  4. 21
      src/CompilerBase.php
  5. 8
      src/Resolver/DeclarationSymbolTrait.php
  6. 12
      src/Resolver/NameResolutionTrait.php
  7. 102
      tests/compiler/namespace/explicit-alias-name-resolution.phpt
  8. 4
      tests/compiler/namespace/mixed-use-imports.phpt

@ -0,0 +1,11 @@
<?php
namespace AliasResolution\Imported;
class Notes {}
namespace AliasResolution\Consumer;
use AliasResolution\Imported\Notes as NotesFactory;
class Child extends Notes {}

@ -1883,7 +1883,7 @@ YAML);
public function testGetNamespacedClassNameWithUseAlias(): void
{
$this->setPropertyValue('useAliases', ['User' => 'App\\Entity\\User']);
$this->setPropertyValue('useAliases', ['user' => 'App\\Entity\\User']);
$this->assertEquals(
'App\\Entity\\User',
$this->compiler->getNamespacedClassName('User')
@ -1892,13 +1892,40 @@ YAML);
public function testGetNamespacedClassNameWithUseAliasSubNamespace(): void
{
$this->setPropertyValue('useAliases', ['Entity' => 'App\\Entity']);
$this->setPropertyValue('useAliases', ['entity' => 'App\\Entity']);
$this->assertEquals(
'App\\Entity\\User',
$this->compiler->getNamespacedClassName('Entity\\User')
);
}
public function testExplicitClassAliasDoesNotImportTheOriginalShortName(): void
{
$use = new \PhpParser\Node\Stmt\Use_([
new \PhpParser\Node\UseItem(
new \PhpParser\Node\Name('Vendor\\Package\\Notes'),
new \PhpParser\Node\Identifier('NotesFactory'),
),
]);
$this->invokeMethod('parseUse', $use);
$this->setPropertyValue('namespace', 'Application\\Api');
$this->assertSame([], $this->getPropertyValue('useNamespaces'));
$this->assertSame(
['notesfactory' => 'Vendor\\Package\\Notes'],
$this->getPropertyValue('useAliases'),
);
$this->assertSame(
'Vendor\\Package\\Notes',
$this->compiler->getNamespacedClassName('NOTESFACTORY'),
);
$this->assertSame(
'Application\\Api\\Notes',
$this->compiler->getNamespacedClassName('Notes'),
);
}
// ========================================================================
// getNamespacedClassName - with use namespace (partial match)
// ========================================================================
@ -1956,7 +1983,7 @@ YAML);
public function testGetNamespacedClassNameAliasPriority(): void
{
$this->setPropertyValue('useAliases', ['User' => 'App\\Models\\User']);
$this->setPropertyValue('useAliases', ['user' => 'App\\Models\\User']);
$this->setPropertyValue('useNamespaces', ['App\\Controllers']);
// Alias should be checked first
$this->assertEquals(

@ -0,0 +1,14 @@
<?php
final class NamespaceAliasResolutionTest extends \BaseTest
{
public function testExplicitAliasDoesNotMakeTheOriginalShortNameAvailable(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);
$this->expectExceptionMessage(
'Class `Child` inherits from a non-existent class `AliasResolution\\Consumer\\Notes`',
);
$this->compile('namespace/explicit-alias-original-name.php');
}
}

@ -3369,29 +3369,20 @@ class CompilerBase implements PropertyAccessContext
if ($funcName[0] == '\\') {
$funcName = ltrim($funcName, '\\');
$possibleFunctionNames = [$this->escapeName($funcName)];
} elseif (str_contains($funcName, '\\')) {
// Qualified function names use the class/namespace import table
// for their first segment, just like qualified class names.
$possibleFunctionNames = [
$this->escapeName($this->getNamespacedClassName($funcName)),
];
} else {
$possibleFunctionNames = [$this->escapeName($funcName)];
if (isset($this->useAliases[$funcName])) {
$possibleFunctionNames[] = $this->escapeName($this->escapeNamespace($this->useAliases[$funcName]));
}
if ($this->namespace) {
$possibleFunctionNames[] = $this->escapeNamespace($this->namespace) . self::NAMESPACE_SEPARATOR . $this->escapeName($funcName);
}
if (isset($this->useFunctions[$funcName])) {
$possibleFunctionNames[] = $this->escapeNamespace($this->useFunctions[$funcName]);
}
// 复杂命名空间规则,组合命名空间
// 例子:use foo\bar; bar\fn();
foreach ($this->useNamespaces as $use) {
$ns1 = explode('\\', $use);
$ns2 = explode('\\', $funcName);
if ($ns1[array_key_last($ns1)] === $ns2[array_key_first($ns2)]) {
$ns = array_merge($ns1, $ns2);
array_splice($ns, array_key_last($ns1) + 1);
$possibleFunctionNames[] = $this->escapeNamespace(implode('\\', $ns));
break;
}
}
}
foreach ($possibleFunctionNames as $nativeFunc) {

@ -103,9 +103,13 @@ trait DeclarationSymbolTrait
} elseif ($idLower === 'bigint_types') {
$this->bigintTypes = true;
} else {
$this->useNamespaces[] = $id;
if ($use->alias) {
$this->useAliases[$use->alias->toString()] = $id;
// Class and namespace import aliases are case-insensitive.
// An explicit alias replaces the implicit short name; it
// must not also make the target's final segment available.
$this->useAliases[strtolower($use->alias->toString())] = $id;
} else {
$this->useNamespaces[] = $id;
}
}
}

@ -29,8 +29,9 @@ trait NameResolutionTrait
$ns2 = explode('\\', trim($class, '\\'));
if (isset($this->useAliases[$ns2[0]])) {
$ns = '\\' . $this->useAliases[$ns2[0]];
$aliasTarget = $this->getClassImportAlias($ns2[0]);
if ($aliasTarget !== null) {
$ns = '\\' . $aliasTarget;
_return:
if (count($ns2) > 1) {
$ns .= '\\' . implode('\\', array_slice($ns2, 1));
@ -105,7 +106,7 @@ trait NameResolutionTrait
}
$resolved = $typeName;
$firstSegment = explode('\\', $typeName, 2)[0];
$hasImportedPrefix = isset($this->useAliases[$firstSegment]);
$hasImportedPrefix = $this->getClassImportAlias($firstSegment) !== null;
if (!$hasImportedPrefix) {
foreach ($this->useNamespaces as $useNamespace) {
$segments = explode('\\', trim($useNamespace, '\\'));
@ -123,6 +124,11 @@ trait NameResolutionTrait
return $type;
}
private function getClassImportAlias(string $name): ?string
{
return $this->useAliases[strtolower($name)] ?? null;
}
/**
* 函数名称处理,补齐 namespace
*/

@ -0,0 +1,102 @@
--TEST--
Explicit class aliases do not import the target short name
--FILE--
<?php
namespace AliasResolution\Imported {
const VALUE = 'qualified-constant';
function label(): string
{
return 'qualified-function';
}
class Notes
{
public const string KIND = 'imported';
public function source(): string
{
return 'imported';
}
}
}
namespace AliasResolution\Consumer {
use AliasResolution\Imported as ImportedNamespace;
use AliasResolution\Imported\Notes as NotesFactory;
class Notes
{
public const string KIND = 'local';
public function source(): string
{
return 'local';
}
}
class LocalChild extends Notes {}
class ImportedChild extends nOtEsFaCtOrY {}
class Holder
{
public Notes $local;
public NOTESFACTORY $imported;
public function __construct(Notes $local, notesfactory $imported)
{
$this->local = $local;
$this->imported = $imported;
}
}
function run(): void
{
$holder = new Holder(new LocalChild(), new ImportedChild());
var_dump($holder->local->source());
var_dump($holder->imported->source());
var_dump(Notes::KIND);
var_dump(notesfactory::KIND);
var_dump(ImportedNamespace\label());
var_dump(ImportedNamespace\VALUE);
}
}
namespace AliasResolution\Grouped {
use AliasResolution\Imported\{Notes as GroupedFactory};
class Notes
{
public function source(): string
{
return 'group-local';
}
}
class LocalChild extends Notes {}
class ImportedChild extends gRoUpEdFaCtOrY {}
function run(): void
{
var_dump((new LocalChild())->source());
var_dump((new ImportedChild())->source());
}
}
namespace {
function main(): void
{
AliasResolution\Consumer\run();
AliasResolution\Grouped\run();
}
}
?>
--EXPECT--
string(5) "local"
string(8) "imported"
string(5) "local"
string(8) "imported"
string(18) "qualified-function"
string(18) "qualified-constant"
string(11) "group-local"
string(8) "imported"

@ -19,8 +19,8 @@ namespace Utils\Math {
}
namespace App {
use Utils\Str\slug;
use Utils\Str\prefix;
use function Utils\Str\slug;
use function Utils\Str\prefix;
use function Utils\Math\double;
class TextHelper {

Loading…
Cancel
Save