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
master
韩天峰 4 days ago
parent c8148018f8
commit 9b02dcd693
  1. 17
      phpunit/code/namespace/import-alias-domains.php
  2. 5
      phpunit/src/ClassTest.php
  3. 48
      phpunit/src/NegativeCompatibilityTest.php
  4. 3
      src/CompilerBase.php
  5. 27
      src/Preprocessor.php
  6. 44
      src/Resolver/DeclarationSymbolTrait.php

@ -0,0 +1,17 @@
<?php
namespace ImportAliasDomains\First;
use Vendor\Package\Route;
use function Vendor\Package\dispatch as Route;
use const Vendor\Package\ROUTE as Route;
use const Vendor\Package\route as route;
class Consumer {}
namespace ImportAliasDomains\Second;
// Import aliases are local to a namespace block.
use Another\Package\Route;
class Consumer {}

@ -2,6 +2,11 @@
class ClassTest extends \BaseTest
{
public function testImportAliasesUseIndependentPhpSymbolDomains(): void
{
$this->compile('namespace/import-alias-domains.php');
}
public function testZendToArrayDeclarationCannotAcceptParameters(): void
{
$this->expectException(\TypePhp\Exception\TestError::class);

@ -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'
<?php
namespace DuplicateImport;
use support\annotation\route\Route;
use Webman\Route\Route; // @diagnostic
function main(): void
{
}
PHP,
];
yield 'duplicate class import alias is case insensitive' => [
'prepare',
'Cannot use Second\\Package\\Route as route because the name is already in use',
<<<'PHP'
<?php
namespace DuplicateImport;
use First\Package\Route as Route;
use Second\Package\Route as route; // @diagnostic
function main(): void
{
}
PHP,
];
yield 'duplicate function import alias is case insensitive' => [
'prepare',
'Cannot use function Second\\Package\\dispatch as handler because the name is already in use',
<<<'PHP'
<?php
namespace DuplicateImport;
use function First\Package\dispatch as Handler;
use function Second\Package\dispatch as handler; // @diagnostic
function main(): void
{
}
PHP,
];
yield 'closure reference parameter' => [
'convert',
'Closure cannot use reference parameter',

@ -409,6 +409,8 @@ class CompilerBase implements PropertyAccessContext
protected array $useAliases = [];
protected array $useFunctions = [];
protected array $useConstants = [];
/** @var array<int, array<string, string>> 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 = '';
}

@ -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.

@ -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);
}

Loading…
Cancel
Save