From e9f617eae00967c49e25832032ac05aea3c177f6 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 27 Jul 2026 18:27:44 +0800 Subject: [PATCH] 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 --- phpunit/src/CompilerBaseApiTest.php | 32 ++++++++- src/Optimizer/FuncCallOptimizer.php | 6 +- src/Parser/ConstantExpressionTrait.php | 67 ++++++++++--------- .../define-runtime-name-compatibility.phpt | 17 +++++ .../runtime-constant-global-fallback.phpt | 41 ++++++++++++ .../runtime-constant-qualified-import.phpt | 45 +++++++++++++ .../runtime-constant-undefined-error.phpt | 25 +++++++ 7 files changed, 196 insertions(+), 37 deletions(-) create mode 100644 tests/compiler/const/define-runtime-name-compatibility.phpt create mode 100644 tests/compiler/namespace/runtime-constant-global-fallback.phpt create mode 100644 tests/compiler/namespace/runtime-constant-qualified-import.phpt create mode 100644 tests/compiler/namespace/runtime-constant-undefined-error.phpt diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index 003c46a4..4939ec83 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -248,10 +248,40 @@ class CompilerBaseApiTest extends TestCase 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); } + 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 { $name = 'AOT_USER_DEFINE_' . str_replace('.', '_', uniqid('', true)); diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index 3adda99a..5e3e4d46 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -724,7 +724,7 @@ trait FuncCallOptimizer protected function genDefine(string $n, Node\Expr\FuncCall $e, array $c): string|false { $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 . '`'); } $args = count($e->args) >= 3 ? 3 : 2; @@ -844,8 +844,4 @@ trait FuncCallOptimizer // Utility // ========================================================================= - protected function isValidDefineName(string $name): bool - { - return preg_match('/^(?!\d)[\p{L}_][\p{L}\p{N}_]*$/u', $name) === 1; - } } diff --git a/src/Parser/ConstantExpressionTrait.php b/src/Parser/ConstantExpressionTrait.php index 60572570..3e9af57c 100644 --- a/src/Parser/ConstantExpressionTrait.php +++ b/src/Parser/ConstantExpressionTrait.php @@ -24,21 +24,6 @@ trait ConstantExpressionTrait } $name = $this->parseIdentifier($expr->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) { return self::VALUE_NULL; } @@ -48,15 +33,6 @@ trait ConstantExpressionTrait if (strcasecmp($name, 'false') === 0) { 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 (str_contains($name, '::')) { $ns = explode('::', $name)[0]; @@ -64,20 +40,49 @@ trait ConstantExpressionTrait $ce = $this->getClassEntryPtr($fullName); return Symbol::constant() . '(' . $ce . ', ' . $this->getLiteralString($ns[1]) . ')'; } - if ($this->isInternalConstant($name)) { - return Symbol::constant() . '(' . $this->getLiteralString($name) . ')'; - } - if (isset($this->useAliases[$name])) { - $name = $this->useAliases[$name]; - } elseif (isset($this->useConstants[$name])) { + + if (isset($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 { $fullName = $this->getNamespacedClassName($name); if ($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) . '")'; } diff --git a/tests/compiler/const/define-runtime-name-compatibility.phpt b/tests/compiler/const/define-runtime-name-compatibility.phpt new file mode 100644 index 00000000..7a91b269 --- /dev/null +++ b/tests/compiler/const/define-runtime-name-compatibility.phpt @@ -0,0 +1,17 @@ +--TEST-- +define accepts namespaced and non-identifier constant names like PHP +--FILE-- + +--EXPECT-- +int(42) +string(9) "supported" diff --git a/tests/compiler/namespace/runtime-constant-global-fallback.phpt b/tests/compiler/namespace/runtime-constant-global-fallback.phpt new file mode 100644 index 00000000..e7b69814 --- /dev/null +++ b/tests/compiler/namespace/runtime-constant-global-fallback.phpt @@ -0,0 +1,41 @@ +--TEST-- +Unqualified runtime constants in a namespace fall back to global constants +--FILE-- + +--EXPECT-- +string(6) "global" +string(10) "namespaced" +string(16) "runtime-override" diff --git a/tests/compiler/namespace/runtime-constant-qualified-import.phpt b/tests/compiler/namespace/runtime-constant-qualified-import.phpt new file mode 100644 index 00000000..6c659efa --- /dev/null +++ b/tests/compiler/namespace/runtime-constant-qualified-import.phpt @@ -0,0 +1,45 @@ +--TEST-- +Qualified and imported runtime constants use their exact resolved names +--FILE-- + +--EXPECT-- +string(8) "imported" +string(9) "qualified" +string(15) "global-constant" diff --git a/tests/compiler/namespace/runtime-constant-undefined-error.phpt b/tests/compiler/namespace/runtime-constant-undefined-error.phpt new file mode 100644 index 00000000..0e3913ed --- /dev/null +++ b/tests/compiler/namespace/runtime-constant-undefined-error.phpt @@ -0,0 +1,25 @@ +--TEST-- +Undefined runtime constants in a namespace throw Error after global fallback +--FILE-- +getMessage(), PHP_EOL; + } + } +} +?> +--EXPECT-- +Undefined constant "RuntimeConstantUndefined\MISSING_RUNTIME_CONSTANT"