From 9b02dcd6936277a668966d6615de21f51327af3e Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 28 Aug 2026 12:09:55 +0800 Subject: [PATCH] feat(compiler): implement independent import alias symbol domains - Add useImportAliases property to track import aliases by type domain - Implement registerUseImportAlias method with duplicate detection logic - Update use declaration processing to utilize import alias registration - Make class and function aliases case-insensitive while constants remain case-sensitive - Add comprehensive test coverage for import alias domain isolation - Include negative test cases for duplicate alias detection across domains - Move AST name resolution earlier in preprocessing steps - Add proper error handling for PHP parser errors during traversal --- .../code/namespace/import-alias-domains.php | 17 +++++++ phpunit/src/ClassTest.php | 5 ++ phpunit/src/NegativeCompatibilityTest.php | 48 +++++++++++++++++++ src/CompilerBase.php | 3 ++ src/Preprocessor.php | 27 ++++++++--- src/Resolver/DeclarationSymbolTrait.php | 44 +++++++++++------ 6 files changed, 122 insertions(+), 22 deletions(-) create mode 100644 phpunit/code/namespace/import-alias-domains.php diff --git a/phpunit/code/namespace/import-alias-domains.php b/phpunit/code/namespace/import-alias-domains.php new file mode 100644 index 00000000..3214da1c --- /dev/null +++ b/phpunit/code/namespace/import-alias-domains.php @@ -0,0 +1,17 @@ +compile('namespace/import-alias-domains.php'); + } + public function testZendToArrayDeclarationCannotAcceptParameters(): void { $this->expectException(\TypePhp\Exception\TestError::class); diff --git a/phpunit/src/NegativeCompatibilityTest.php b/phpunit/src/NegativeCompatibilityTest.php index 3a968a76..5fdfc65a 100644 --- a/phpunit/src/NegativeCompatibilityTest.php +++ b/phpunit/src/NegativeCompatibilityTest.php @@ -145,6 +145,54 @@ function main(): void PHP, ]; + yield 'duplicate implicit class import' => [ + 'prepare', + 'Cannot use Webman\\Route\\Route as Route because the name is already in use', + <<<'PHP' + [ + 'prepare', + 'Cannot use Second\\Package\\Route as route because the name is already in use', + <<<'PHP' + [ + 'prepare', + 'Cannot use function Second\\Package\\dispatch as handler because the name is already in use', + <<<'PHP' + [ 'convert', 'Closure cannot use reference parameter', diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 0de8db77..b8a36550 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -409,6 +409,8 @@ class CompilerBase implements PropertyAccessContext protected array $useAliases = []; protected array $useFunctions = []; protected array $useConstants = []; + /** @var array> Import aliases separated by class/function/constant domain. */ + protected array $useImportAliases = []; /** * 原始类名,不包含命名空间. @@ -1033,6 +1035,7 @@ class CompilerBase implements PropertyAccessContext $this->useAliases = []; $this->useFunctions = []; $this->useConstants = []; + $this->useImportAliases = []; $this->namespace = ''; } diff --git a/src/Preprocessor.php b/src/Preprocessor.php index a108febe..2007a9db 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -74,15 +74,15 @@ class Preprocessor extends CompilerBase } try { $ast = $this->parser->parse($source); + $traverser = new NodeTraverser(); + $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); + $ast = $this->requireStatementList($traverser->traverse($ast)); } catch (\PhpParser\Error) { // prepareFile() owns the normal source diagnostic, including // the filename and compiler formatting. Avoid reporting a // syntax error twice from this declaration-only pass. continue; } - $traverser = new NodeTraverser(); - $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); - $ast = $traverser->traverse($ast); $this->discoverNativeClassDeclarationsInAst($ast); } } @@ -161,14 +161,14 @@ class Preprocessor extends CompilerBase foreach ($candidateSources as $source) { try { $ast = $this->parser->parse($source); + $traverser = new NodeTraverser(); + $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); + $ast = $this->requireStatementList($traverser->traverse($ast)); } catch (\PhpParser\Error) { // prepareFile() has already emitted the authoritative syntax // diagnostic. This pass must not report it a second time. continue; } - $traverser = new NodeTraverser(); - $traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false])); - $ast = $traverser->traverse($ast); foreach ($discovery->discover($ast) as $slot) { $this->registerNativeGlobalObject($slot['name'], $slot['class'], $slot['node']); } @@ -307,7 +307,11 @@ class Preprocessor extends CompilerBase $this->file, fn (string $class, string $case): bool => $this->isDeclaredEnumCase($class, $case), )); - $stmts = $this->requireStatementList($traverser->traverse($ast)); + try { + $stmts = $this->requireStatementList($traverser->traverse($ast)); + } catch (\PhpParser\Error $error) { + $this->fatalPhpParserError($error); + } // Keep the resolved declaration AST until convert. Defaults and // constants are validated here, but their C++ expressions are not // generated until the complete symbol table is available. @@ -344,6 +348,15 @@ class Preprocessor extends CompilerBase } } + protected function fatalPhpParserError(\PhpParser\Error $error): never + { + $location = $this->file; + if ($error->getStartLine() > 0) { + $location .= ':' . $error->getStartLine(); + } + $this->error($error->getRawMessage() . ' in ' . $location); + } + /** * Root parser output must remain a statement list after declaration * visitors have run. Validate that invariant before storing the AST. diff --git a/src/Resolver/DeclarationSymbolTrait.php b/src/Resolver/DeclarationSymbolTrait.php index 70016fbc..b4c1da57 100644 --- a/src/Resolver/DeclarationSymbolTrait.php +++ b/src/Resolver/DeclarationSymbolTrait.php @@ -76,24 +76,15 @@ trait DeclarationSymbolTrait foreach ($v2->uses as $use) { $id = $this->parseIdentifier($use->name); $type = $use->type !== Node\Stmt\Use_::TYPE_UNKNOWN ? $use->type : $v2->type; + $alias = $this->registerUseImportAlias($use, $type, $id); if ($type === Node\Stmt\Use_::TYPE_FUNCTION) { - $lastIndex = strrpos($id, '\\'); - $fn = substr($id, $lastIndex + 1); - if ($use->alias) { - $this->useFunctions[$use->alias->toString()] = $id; - } else { - $this->useFunctions[$fn] = $id; - } + $this->useFunctions[$alias] = $id; } elseif ($type === Node\Stmt\Use_::TYPE_CONSTANT) { $lastIndex = strrpos($id, '\\'); $cn = substr($id, $lastIndex + 1); $ns = substr($id, 0, $lastIndex); $fullName = $ns . '\\' . $cn; - if ($use->alias) { - $this->useConstants[$use->alias->toString()] = $fullName; - } else { - $this->useConstants[$cn] = $fullName; - } + $this->useConstants[$alias] = $fullName; } else { $idLower = strtolower($id); if ($idLower === 'native_types') { @@ -107,7 +98,7 @@ trait DeclarationSymbolTrait // 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; + $this->useAliases[strtolower($alias)] = $id; } else { $this->useNamespaces[] = $id; } @@ -116,15 +107,38 @@ trait DeclarationSymbolTrait } } + private function registerUseImportAlias(Node\UseItem $use, int $type, string $id): string + { + $alias = $use->getAlias()->toString(); + // PHP class and function names are case-insensitive, while constant + // aliases are case-sensitive. The three import kinds have independent + // symbol tables, so the same alias may be used once in each domain. + $key = $type === Node\Stmt\Use_::TYPE_CONSTANT ? $alias : strtolower($alias); + if (isset($this->useImportAliases[$type][$key])) { + $kind = match ($type) { + Node\Stmt\Use_::TYPE_FUNCTION => 'function ', + Node\Stmt\Use_::TYPE_CONSTANT => 'const ', + default => '', + }; + $this->fatalError( + $use, + "Cannot use {$kind}{$id} as {$alias} because the name is already in use", + ); + } + $this->useImportAliases[$type][$key] = $id; + + return $alias; + } + protected function parseGroupUse(Node\Stmt\GroupUse $node): void { $prefix = $node->prefix; $uses = []; foreach ($node->uses as $use) { $fullName = Node\Name::concat($prefix, $use->name); - $uses[] = new Node\UseItem($fullName, $use->alias, $use->type); + $uses[] = new Node\UseItem($fullName, $use->alias, $use->type, $use->getAttributes()); } - $syntheticUse = new Node\Stmt\Use_($uses, $node->type); + $syntheticUse = new Node\Stmt\Use_($uses, $node->type, $node->getAttributes()); $this->parseUse($syntheticUse); }