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
pull/48/head
韩天峰 2 weeks ago
parent 997bbffb65
commit 6fb8187b07
  1. 1
      .gitignore
  2. 31
      src/Parser/ConstantExpressionTrait.php
  3. 17
      tests/compiler/namespace/runtime-constant-global-fallback.phpt
  4. 41
      tests/compiler/static/static-call-branch-carrier.phpt

1
.gitignore vendored

@ -32,3 +32,4 @@ tests/**/*.sh
/*.browser/ /*.browser/
/tests/wasm/harness/node_modules/ /tests/wasm/harness/node_modules/
/ide-helper/ /ide-helper/
/debug/

@ -133,18 +133,37 @@ trait ConstantExpressionTrait
protected function detectConstType($expr): string protected function detectConstType($expr): string
{ {
$name = $this->parseIdentifier($expr->name); $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)) { if ($this->hasConstant($name)) {
return $this->getConstantType($name); return $this->getConstantType($name);
} }
if ($this->isInternalConstant($name)) { if ($this->isInternalConstant($name)) {
return $this->getTypeFromZendType(gettype($this->internalConstants[$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') { if ($name === 'NAN' or $name === 'INF') {
return Type::FLOAT; return Type::FLOAT;
} }

@ -4,6 +4,20 @@ Unqualified runtime constants in a namespace fall back to global constants
<?php <?php
namespace RuntimeConstantFallback { namespace RuntimeConstantFallback {
class InternalConstantTernary
{
private bool $useLimit = true;
private array $values = [1, 2, 3];
public function read(): int
{
// Reading the property introduces branch cleanup in generated C++.
// The unqualified constant must still be treated as a dynamic value:
// a namespaced definition can shadow PHP's global internal constant.
return $this->useLimit ? PHP_INT_MAX : count($this->values);
}
}
function readGlobalOnly() function readGlobalOnly()
{ {
return GLOBAL_ONLY; return GLOBAL_ONLY;
@ -28,10 +42,12 @@ namespace {
define('RuntimeConstantFallback\Preferred', 'wrong-case-name'); define('RuntimeConstantFallback\Preferred', 'wrong-case-name');
define('RuntimeConstantFallback\PREFERRED', 'namespaced'); define('RuntimeConstantFallback\PREFERRED', 'namespaced');
define('RuntimeConstantFallback\PHP_VERSION', 'runtime-override'); define('RuntimeConstantFallback\PHP_VERSION', 'runtime-override');
define('RuntimeConstantFallback\PHP_INT_MAX', 42);
var_dump(\RuntimeConstantFallback\readGlobalOnly()); var_dump(\RuntimeConstantFallback\readGlobalOnly());
var_dump(\RuntimeConstantFallback\readPreferred()); var_dump(\RuntimeConstantFallback\readPreferred());
var_dump(\RuntimeConstantFallback\readInternalOverride()); var_dump(\RuntimeConstantFallback\readInternalOverride());
var_dump((new \RuntimeConstantFallback\InternalConstantTernary())->read());
} }
} }
?> ?>
@ -39,3 +55,4 @@ namespace {
string(6) "global" string(6) "global"
string(10) "namespaced" string(10) "namespaced"
string(16) "runtime-override" string(16) "runtime-override"
int(42)

@ -0,0 +1,41 @@
--TEST--
Native static calls initialize the class carrier outside conditional branches
--FILE--
<?php
class StaticBranchCarrier
{
protected static array $cache = [];
public static function normalize(string $value): string
{
if (isset(static::$cache[$value])) {
return static::$cache[$value];
}
return static::$cache[$value] = strtoupper($value);
}
}
function selectBranch(bool $first): string
{
if ($first) {
return StaticBranchCarrier::normalize('first');
} else {
return StaticBranchCarrier::normalize('second');
}
}
function main(): void
{
// The first static call in source order is deliberately not executed.
var_dump(selectBranch(false));
var_dump(selectBranch(true));
var_dump(selectBranch(false));
}
?>
--EXPECT--
string(6) "SECOND"
string(5) "FIRST"
string(6) "SECOND"
Loading…
Cancel
Save