diff --git a/phpunit/code/namespace/explicit-alias-original-name.php b/phpunit/code/namespace/explicit-alias-original-name.php new file mode 100644 index 00000000..3ecdfc9d --- /dev/null +++ b/phpunit/code/namespace/explicit-alias-original-name.php @@ -0,0 +1,11 @@ +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( diff --git a/phpunit/src/NamespaceAliasResolutionTest.php b/phpunit/src/NamespaceAliasResolutionTest.php new file mode 100644 index 00000000..fa862933 --- /dev/null +++ b/phpunit/src/NamespaceAliasResolutionTest.php @@ -0,0 +1,14 @@ +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'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index f73ce292..5c545cae 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.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) { diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index ea3aa2e6..70016fbc 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -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; } } } diff --git a/src/Resolver/NameResolutionTrait.php b/src/Resolver/NameResolutionTrait.php index 827db4a3..4c21e5a4 100644 --- a/src/Resolver/NameResolutionTrait.php +++ b/src/Resolver/NameResolutionTrait.php @@ -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 */ diff --git a/tests/compiler/namespace/explicit-alias-name-resolution.phpt b/tests/compiler/namespace/explicit-alias-name-resolution.phpt new file mode 100644 index 00000000..b73ae884 --- /dev/null +++ b/tests/compiler/namespace/explicit-alias-name-resolution.phpt @@ -0,0 +1,102 @@ +--TEST-- +Explicit class aliases do not import the target short name +--FILE-- +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" diff --git a/tests/compiler/namespace/mixed-use-imports.phpt b/tests/compiler/namespace/mixed-use-imports.phpt index 67775813..16a9f30b 100644 --- a/tests/compiler/namespace/mixed-use-imports.phpt +++ b/tests/compiler/namespace/mixed-use-imports.phpt @@ -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 {