From 6fb8187b077ca99d8db89ffcd03733618db68739 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 10 Aug 2026 21:44:36 +0800 Subject: [PATCH] fix(parser): handle constant expression type detection with namespace fallback - Strip leading backslashes from constant names before processing - Add special handling for true/false language constants to prevent namespace fallback - Implement proper namespace resolution for unqualified constants - Respect use statement aliases for constant names - Return VAR type when namespaced constants may shadow globals at runtime - Move true/false checks to occur before namespace resolution - Add comprehensive test coverage for runtime constant fallback behavior - Add test case for static method calls with branch carriers --- .gitignore | 1 + src/Parser/ConstantExpressionTrait.php | 31 +++++++++++--- .../runtime-constant-global-fallback.phpt | 17 ++++++++ .../static/static-call-branch-carrier.phpt | 41 +++++++++++++++++++ 4 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 tests/compiler/static/static-call-branch-carrier.phpt diff --git a/.gitignore b/.gitignore index 3d75269c..b43a589e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ tests/**/*.sh /*.browser/ /tests/wasm/harness/node_modules/ /ide-helper/ +/debug/ diff --git a/src/Parser/ConstantExpressionTrait.php b/src/Parser/ConstantExpressionTrait.php index ad61bcad..8acc2458 100644 --- a/src/Parser/ConstantExpressionTrait.php +++ b/src/Parser/ConstantExpressionTrait.php @@ -133,18 +133,37 @@ trait ConstantExpressionTrait protected function detectConstType($expr): string { $name = $this->parseIdentifier($expr->name); + $name = ltrim($name, '\\'); + + // true and false are language constants and never participate in + // namespace fallback. Keep type resolution consistent with + // parseConstFetch(), which handles them before resolving the name. + if (strcasecmp($name, 'true') === 0 || strcasecmp($name, 'false') === 0) { + return Type::BOOL; + } + + if (isset($this->useConstants[$name])) { + $name = $this->useConstants[$name]; + } elseif ($expr->name->isUnqualified() && $this->namespace) { + $namespacedName = $this->namespace . '\\' . $name; + if ($this->hasConstant($namespacedName)) { + return $this->getConstantType($namespacedName); + } + + // PHP checks Namespace\NAME before falling back to global NAME. + // A runtime define() can therefore shadow even an internal global + // constant, so its type cannot be inferred statically here. + return Type::VAR; + } elseif (!($expr->name instanceof Node\Name\FullyQualified)) { + $name = $this->getNamespacedClassName($name); + } + if ($this->hasConstant($name)) { return $this->getConstantType($name); } if ($this->isInternalConstant($name)) { return $this->getTypeFromZendType(gettype($this->internalConstants[$name])); } - if (strcasecmp($name, 'true') === 0) { - return Type::BOOL; - } - if (strcasecmp($name, 'false') === 0) { - return Type::BOOL; - } if ($name === 'NAN' or $name === 'INF') { return Type::FLOAT; } diff --git a/tests/compiler/namespace/runtime-constant-global-fallback.phpt b/tests/compiler/namespace/runtime-constant-global-fallback.phpt index e7b69814..e13112bc 100644 --- a/tests/compiler/namespace/runtime-constant-global-fallback.phpt +++ b/tests/compiler/namespace/runtime-constant-global-fallback.phpt @@ -4,6 +4,20 @@ Unqualified runtime constants in a namespace fall back to global constants useLimit ? PHP_INT_MAX : count($this->values); + } + } + function readGlobalOnly() { return GLOBAL_ONLY; @@ -28,10 +42,12 @@ namespace { define('RuntimeConstantFallback\Preferred', 'wrong-case-name'); define('RuntimeConstantFallback\PREFERRED', 'namespaced'); define('RuntimeConstantFallback\PHP_VERSION', 'runtime-override'); + define('RuntimeConstantFallback\PHP_INT_MAX', 42); var_dump(\RuntimeConstantFallback\readGlobalOnly()); var_dump(\RuntimeConstantFallback\readPreferred()); var_dump(\RuntimeConstantFallback\readInternalOverride()); + var_dump((new \RuntimeConstantFallback\InternalConstantTernary())->read()); } } ?> @@ -39,3 +55,4 @@ namespace { string(6) "global" string(10) "namespaced" string(16) "runtime-override" +int(42) diff --git a/tests/compiler/static/static-call-branch-carrier.phpt b/tests/compiler/static/static-call-branch-carrier.phpt new file mode 100644 index 00000000..fff52d77 --- /dev/null +++ b/tests/compiler/static/static-call-branch-carrier.phpt @@ -0,0 +1,41 @@ +--TEST-- +Native static calls initialize the class carrier outside conditional branches +--FILE-- + +--EXPECT-- +string(6) "SECOND" +string(5) "FIRST" +string(6) "SECOND"