feat(parser): implement runtime constant resolution with namespace fallback

- Remove static constant lookup logic from parseConstFetch method
- Add support for unqualified constants in namespaces with runtime fallback mechanism
- Implement UnqualifiedInNamespace constant lookup strategy for proper PHP behavior
- Update test cases to verify runtime constant resolution works correctly
- Add comprehensive test coverage for namespace constant fallback scenarios
- Remove isValidDefineName validation in favor of direct string checking
- Support define() calls with namespaced and non-identifier constant names
- Add proper error handling for undefined constants in namespaces
pull/40/head
韩天峰 4 weeks ago
parent 0b1a45660e
commit e9f617eae0
  1. 32
      phpunit/src/CompilerBaseApiTest.php
  2. 6
      src/Optimizer/FuncCallOptimizer.php
  3. 67
      src/Parser/ConstantExpressionTrait.php
  4. 17
      tests/compiler/const/define-runtime-name-compatibility.phpt
  5. 41
      tests/compiler/namespace/runtime-constant-global-fallback.phpt
  6. 45
      tests/compiler/namespace/runtime-constant-qualified-import.phpt
  7. 25
      tests/compiler/namespace/runtime-constant-undefined-error.phpt

@ -248,10 +248,40 @@ class CompilerBaseApiTest extends TestCase
new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('ROOT_PATH')) new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('ROOT_PATH'))
); );
$this->assertStringStartsWith('php::constant(nullptr, ', $code); $this->assertStringStartsWith('php::constant(', $code);
$this->assertStringNotContainsString(ROOT_PATH, $code); $this->assertStringNotContainsString(ROOT_PATH, $code);
} }
public function testUnqualifiedRuntimeConstantUsesNamespaceFallback(): void
{
$this->setPropertyValue('namespace', 'App\\Worker');
$this->setPropertyValue('noLiteralStrings', true);
$code = $this->invokeMethod(
'parseConstFetch',
new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('COMPOSER_PATH'))
);
$this->assertStringNotContainsString('php::fn::defined(', $code);
$this->assertStringContainsString('App\\\\Worker\\\\COMPOSER_PATH', $code);
$this->assertStringEndsWith(', php::ConstantLookup::UnqualifiedInNamespace)', $code);
$this->assertStringNotContainsString('php::constant(nullptr,', $code);
}
public function testQualifiedRuntimeConstantDoesNotUseGlobalFallback(): void
{
$this->setPropertyValue('namespace', 'App\\Worker');
$this->setPropertyValue('noLiteralStrings', true);
$code = $this->invokeMethod(
'parseConstFetch',
new \PhpParser\Node\Expr\ConstFetch(new \PhpParser\Node\Name('Config\\PATH'))
);
$this->assertStringNotContainsString('php::fn::defined(', $code);
$this->assertStringContainsString('App\\\\Worker\\\\Config\\\\PATH', $code);
}
public function testDynamicallyDefinedConstantsAreNotInternalConstants(): void public function testDynamicallyDefinedConstantsAreNotInternalConstants(): void
{ {
$name = 'AOT_USER_DEFINE_' . str_replace('.', '_', uniqid('', true)); $name = 'AOT_USER_DEFINE_' . str_replace('.', '_', uniqid('', true));

@ -724,7 +724,7 @@ trait FuncCallOptimizer
protected function genDefine(string $n, Node\Expr\FuncCall $e, array $c): string|false protected function genDefine(string $n, Node\Expr\FuncCall $e, array $c): string|false
{ {
$arg = $e->args[0]->value; $arg = $e->args[0]->value;
if ($this->isScalarString($arg) && !$this->isValidDefineName($arg->value)) { if ($this->isScalarString($arg) && str_contains($arg->value, '::')) {
$this->fatalError($e, 'Invalid define name `' . $arg->value . '`'); $this->fatalError($e, 'Invalid define name `' . $arg->value . '`');
} }
$args = count($e->args) >= 3 ? 3 : 2; $args = count($e->args) >= 3 ? 3 : 2;
@ -844,8 +844,4 @@ trait FuncCallOptimizer
// Utility // Utility
// ========================================================================= // =========================================================================
protected function isValidDefineName(string $name): bool
{
return preg_match('/^(?!\d)[\p{L}_][\p{L}\p{N}_]*$/u', $name) === 1;
}
} }

@ -24,21 +24,6 @@ trait ConstantExpressionTrait
} }
$name = $this->parseIdentifier($expr->name); $name = $this->parseIdentifier($expr->name);
$name = ltrim($name, '\\'); $name = ltrim($name, '\\');
if ($this->isNameExpr($expr->name) and $this->hasConstant($name)) {
return $this->getConstant($name);
}
if ($this->namespace and $this->isNameExpr($expr->name) and !$expr->name instanceof Node\Name\FullyQualified) {
$nsName = $this->namespace . '\\' . $name;
if ($this->hasConstant($nsName)) {
return $this->getConstant($nsName);
}
}
if ($this->isNameExpr($expr->name) and isset($this->useConstants[$name])) {
$importedName = $this->useConstants[$name];
if ($this->hasConstant($importedName)) {
return $this->getConstant($importedName);
}
}
if (strcasecmp($name, 'null') === 0) { if (strcasecmp($name, 'null') === 0) {
return self::VALUE_NULL; return self::VALUE_NULL;
} }
@ -48,15 +33,6 @@ trait ConstantExpressionTrait
if (strcasecmp($name, 'false') === 0) { if (strcasecmp($name, 'false') === 0) {
return 'false'; return 'false';
} }
if ($name === 'PHP_EOL') {
return '"' . $this->escapeString(PHP_EOL) . '"';
}
if ($this->isInternalScalarConstant($name)) {
return $this->getInternalScalarConstantValue($name);
}
if ($scalar) {
return constant($expr->name);
}
if ($this->isNameExpr($expr->name)) { if ($this->isNameExpr($expr->name)) {
if (str_contains($name, '::')) { if (str_contains($name, '::')) {
$ns = explode('::', $name)[0]; $ns = explode('::', $name)[0];
@ -64,20 +40,49 @@ trait ConstantExpressionTrait
$ce = $this->getClassEntryPtr($fullName); $ce = $this->getClassEntryPtr($fullName);
return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($ns[1]) . ')'; return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($ns[1]) . ')';
} }
if ($this->isInternalConstant($name)) {
return Symbol::constant() . '(' . $this->getLiteralString($name) . ')'; if (isset($this->useConstants[$name])) {
}
if (isset($this->useAliases[$name])) {
$name = $this->useAliases[$name];
} elseif (isset($this->useConstants[$name])) {
$name = $this->useConstants[$name]; $name = $this->useConstants[$name];
} elseif ($expr->name->isUnqualified()) {
if ($this->namespace) {
$namespacedName = $this->namespace . '\\' . $name;
if ($this->hasConstant($namespacedName)) {
return $this->getConstant($namespacedName);
}
// PHP resolves an unqualified constant in a namespace at
// runtime: first Namespace\NAME, then the global NAME. AOT
// cannot select only the namespaced spelling because a
// define() call may execute before this fetch.
return Symbol::constant() . '('
. $this->getLiteralString($namespacedName)
. ', php::ConstantLookup::UnqualifiedInNamespace)';
}
// A class import with the same alias does not affect a bare
// constant fetch. Only `use const` participates here.
} elseif ($expr->name instanceof Node\Name\FullyQualified) {
// parseIdentifier() has already removed the leading slash.
} else { } else {
$fullName = $this->getNamespacedClassName($name); $fullName = $this->getNamespacedClassName($name);
if ($fullName) { if ($fullName) {
$name = $fullName; $name = $fullName;
} }
} }
return Symbol::constant() . '(nullptr, ' . $this->getLiteralString($name) . ')';
if ($this->hasConstant($name)) {
return $this->getConstant($name);
}
if ($name === 'PHP_EOL') {
return '"' . $this->escapeString(PHP_EOL) . '"';
}
if ($this->isInternalScalarConstant($name)) {
return $this->getInternalScalarConstantValue($name);
}
if ($this->isInternalConstant($name)) {
return Symbol::constant() . '(' . $this->getLiteralString($name) . ')';
}
return Symbol::constant() . '(' . $this->getLiteralString($name) . ')';
} }
return Symbol::constant() . '("' . $this->escapeString($name) . '")'; return Symbol::constant() . '("' . $this->escapeString($name) . '")';
} }

@ -0,0 +1,17 @@
--TEST--
define accepts namespaced and non-identifier constant names like PHP
--FILE--
<?php
function main(): void
{
define('Vendor\Package\VALUE', 42);
define('1 non identifier', 'supported');
var_dump(constant('Vendor\Package\VALUE'));
var_dump(constant('1 non identifier'));
}
?>
--EXPECT--
int(42)
string(9) "supported"

@ -0,0 +1,41 @@
--TEST--
Unqualified runtime constants in a namespace fall back to global constants
--FILE--
<?php
namespace RuntimeConstantFallback {
function readGlobalOnly()
{
return GLOBAL_ONLY;
}
function readPreferred()
{
return PREFERRED;
}
function readInternalOverride()
{
return PHP_VERSION;
}
}
namespace {
function main(): void
{
define('GLOBAL_ONLY', 'global');
define('PREFERRED', 'global-preferred');
define('RuntimeConstantFallback\Preferred', 'wrong-case-name');
define('RuntimeConstantFallback\PREFERRED', 'namespaced');
define('RuntimeConstantFallback\PHP_VERSION', 'runtime-override');
var_dump(\RuntimeConstantFallback\readGlobalOnly());
var_dump(\RuntimeConstantFallback\readPreferred());
var_dump(\RuntimeConstantFallback\readInternalOverride());
}
}
?>
--EXPECT--
string(6) "global"
string(10) "namespaced"
string(16) "runtime-override"

@ -0,0 +1,45 @@
--TEST--
Qualified and imported runtime constants use their exact resolved names
--FILE--
<?php
namespace RuntimeConstantConsumer {
use const RuntimeConstantProvider\IMPORTED_VALUE as IMPORTED;
use RuntimeConstantProvider\CLASS_ALIAS_COLLISION;
function readImported()
{
return IMPORTED;
}
function readQualified()
{
return Config\PATH;
}
function readClassAliasCollision()
{
return CLASS_ALIAS_COLLISION;
}
}
namespace {
function main(): void
{
define('RuntimeConstantProvider\IMPORTED_VALUE', 'imported');
define('RuntimeConstantConsumer\Config\PATH', 'qualified');
define('CLASS_ALIAS_COLLISION', 'global-constant');
define('RuntimeConstantProvider\CLASS_ALIAS_COLLISION', 'wrong-provider');
define('IMPORTED', 'wrong-global');
define('Config\PATH', 'wrong-qualified');
var_dump(\RuntimeConstantConsumer\readImported());
var_dump(\RuntimeConstantConsumer\readQualified());
var_dump(\RuntimeConstantConsumer\readClassAliasCollision());
}
}
?>
--EXPECT--
string(8) "imported"
string(9) "qualified"
string(15) "global-constant"

@ -0,0 +1,25 @@
--TEST--
Undefined runtime constants in a namespace throw Error after global fallback
--FILE--
<?php
namespace RuntimeConstantUndefined {
function readMissing()
{
return MISSING_RUNTIME_CONSTANT;
}
}
namespace {
function main(): void
{
try {
\RuntimeConstantUndefined\readMissing();
} catch (\Error $error) {
echo $error->getMessage(), PHP_EOL;
}
}
}
?>
--EXPECT--
Undefined constant "RuntimeConstantUndefined\MISSING_RUNTIME_CONSTANT"
Loading…
Cancel
Save