fix(parser): resolve PHP_INT_MAX/MIN folds case-sensitively with namespace rules (#49) --skip-tests

constantNumericValue() matched strtolower($name) with no namespace
resolution, so two invalid folds happened: `namespace N;
const PHP_INT_MAX = 5; PHP_INT_MAX + 1` folded to 9.22e18 where PHP
resolves the namespaced constant and yields 6, and a lowercase
`php_int_max` silently folded to the global value where PHP raises an
undefined-constant Error.

Resolve the fetched name the way parseConstFetch() does: a `use const`
alias resolves to its target, a fully qualified name is global, an
unqualified name inside a namespace participates in PHP's runtime
fallback (Namespace\NAME can be defined before the fetch executes) and
therefore never provably names the global, and the match is now
case-sensitive. Only a provable global PHP_INT_MAX/PHP_INT_MIN folds.
master
Alessio Giacobbe 24 hours ago committed by GitHub
parent 284abddba6
commit d00c63a049
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 11
      phpunit/code/php-int-max-fold-global.php
  2. 15
      phpunit/code/php-int-max-fold-namespace.php
  3. 50
      phpunit/src/PhpIntMaxFoldTest.php
  4. 37
      src/Parser/BinaryOpTrait.php
  5. 32
      tests/compiler/const/php-int-max-namespace-shadow.phpt

@ -0,0 +1,11 @@
<?php
function lowercaseIsRuntime()
{
return php_int_max + 1;
}
function uppercaseFolds(): float
{
return PHP_INT_MAX + 1;
}

@ -0,0 +1,15 @@
<?php
namespace FoldNs;
const PHP_INT_MAX = 5;
function shadowedFold(): int
{
return PHP_INT_MAX + 1;
}
function globalFold(): float
{
return \PHP_INT_MAX + 1;
}

@ -0,0 +1,50 @@
<?php
use TypePhp\CompilerTest;
/**
* The PHP_INT_MAX/PHP_INT_MIN constant folder must resolve the constant name
* like PHP does: case-sensitively, and only to the real global constant. An
* unqualified fetch inside a namespace resolves to Namespace\PHP_INT_MAX
* first, and a lowercase php_int_max is an undefined constant, not the
* global value.
*/
final class PhpIntMaxFoldTest extends \BaseTest
{
public function testNamespacedConstantShadowsGlobalAndIsNotFolded(): void
{
$code = $this->compileFixture('php-int-max-fold-namespace.php');
// The unqualified fetch reads the namespaced constant at runtime.
self::assertStringContainsString('_const_var_FoldNs__PHP_INT_MAX', $code);
// The fully qualified fetch still folds to the overflowed float.
self::assertStringContainsString('9.2233720368547758e+18', $code);
}
public function testLowercaseNameIsARuntimeConstantLookup(): void
{
$code = $this->compileFixture('php-int-max-fold-global.php');
// php_int_max is undefined in PHP; it must stay a runtime lookup
// that raises the undefined-constant Error, never fold.
self::assertStringContainsString('php::constant(', $code);
// The exact-case global fetch keeps folding.
self::assertStringContainsString('9.2233720368547758e+18', $code);
}
private function compileFixture(string $file): string
{
global $translator;
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$source = TYPEPHP_ROOT_PATH . '/phpunit/code/' . $file;
$compiler->addFiles([$source]);
$compiler->prepareFile($source);
$generated = $compiler->convertFile($source);
$code = file_get_contents($generated);
self::assertIsString($code);
return $code;
}
}

@ -393,10 +393,13 @@ trait BinaryOpTrait
return $value === null ? null : -$value;
}
if ($expr instanceof Node\Expr\ConstFetch) {
$name = strtolower($expr->name->toString());
// PHP constants are case-sensitive and unqualified names resolve
// through the namespace first, so only a fetch that provably
// names the global constant may fold to its value.
$name = $this->resolveGlobalFoldableConstantName($expr);
return match ($name) {
'php_int_max' => PHP_INT_MAX,
'php_int_min' => PHP_INT_MIN,
'PHP_INT_MAX' => PHP_INT_MAX,
'PHP_INT_MIN' => PHP_INT_MIN,
default => null,
};
}
@ -441,6 +444,34 @@ trait BinaryOpTrait
};
}
/**
* Resolve a constant fetch to the global constant name it provably
* denotes, or null when the fetch may refer to something else.
*
* A `use const` alias resolves to its target. A fully qualified name is
* already global. An unqualified name inside a namespace participates in
* PHP's runtime fallback (Namespace\NAME can be defined before the fetch
* executes), so it never provably names the global constant. A qualified
* relative name resolves inside a namespace/import and is never global.
*/
protected function resolveGlobalFoldableConstantName(Node\Expr\ConstFetch $expr): ?string
{
$name = ltrim($expr->name->toString(), '\\');
if (isset($this->useConstants[$name])) {
return ltrim($this->useConstants[$name], '\\');
}
if ($expr->name instanceof Node\Name\FullyQualified) {
return $name;
}
if (!$expr->name->isUnqualified()) {
return null;
}
if ($this->namespace) {
return null;
}
return $name;
}
protected function constantDivisionValue(int|float $left, int|float $right, bool $nativeSemantics): int|float|null
{
if ($right == 0) {

@ -0,0 +1,32 @@
--TEST--
Namespaced PHP_INT_MAX shadows the global constant in unqualified fetches
--FILE--
<?php
namespace N {
const PHP_INT_MAX = 5;
function shadowed(): int
{
return PHP_INT_MAX + 1;
}
function globalValue(): float
{
return \PHP_INT_MAX + 1;
}
}
namespace {
function main(): void
{
var_dump(\N\shadowed());
var_dump(\N\globalValue());
var_dump(PHP_INT_MAX + 1);
}
}
?>
--EXPECT--
int(6)
float(9.223372036854776E+18)
float(9.223372036854776E+18)
Loading…
Cancel
Save