From 88ba4780c908f215c68a5cb38762f5dd107de300 Mon Sep 17 00:00:00 2001 From: yavon007 Date: Tue, 22 Sep 2026 15:31:01 +0800 Subject: [PATCH] Restrict array_key_exists optimization to integer and string keys (#130) --- .../code/array-key-exists-optimization.php | 23 +++++ .../src/ArrayKeyExistsOptimizationTest.php | 33 +++++++ src/Optimizer/FuncCallOptimizer.php | 5 ++ .../stdlib/array-key-exists-key-types.phpt | 90 +++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 phpunit/code/array-key-exists-optimization.php create mode 100644 phpunit/src/ArrayKeyExistsOptimizationTest.php create mode 100644 tests/compiler/stdlib/array-key-exists-key-types.phpt diff --git a/phpunit/code/array-key-exists-optimization.php b/phpunit/code/array-key-exists-optimization.php new file mode 100644 index 00000000..25003703 --- /dev/null +++ b/phpunit/code/array-key-exists-optimization.php @@ -0,0 +1,23 @@ +keyExists($mixed); +} diff --git a/phpunit/src/ArrayKeyExistsOptimizationTest.php b/phpunit/src/ArrayKeyExistsOptimizationTest.php new file mode 100644 index 00000000..c3f97b68 --- /dev/null +++ b/phpunit/src/ArrayKeyExistsOptimizationTest.php @@ -0,0 +1,33 @@ +addFiles([$source]); + $compiler->prepareFile($source); + $code = file_get_contents($compiler->convertFile($source)); + + self::assertIsString($code); + self::assertSame(2, substr_count($code, '.offsetExists(')); + self::assertSame(8, substr_count($code, 'php::call(')); + self::assertStringNotContainsString('php::fn::array_key_exists(', $code); + } +} diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index f83f4653..aecdafd6 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -1177,6 +1177,11 @@ trait FuncCallOptimizer if (!$this->hasOptimizerSafeReflectedArguments($n, $e, $c)) { return false; } + // offsetExists only matches PHP key semantics for integer/string keys. + // Other keys need Zend's conversions, diagnostics and TypeErrors. + if (!in_array($this->detectTypeOfExpr($e->args[0]->value), [Type::INT, Type::STR], true)) { + return false; + } // The C++ receiver is PHP's second argument, but PHP still evaluates // the key first. Resolve both in source order before rearranging them. $key = $this->getArg($e, 0); diff --git a/tests/compiler/stdlib/array-key-exists-key-types.phpt b/tests/compiler/stdlib/array-key-exists-key-types.phpt new file mode 100644 index 00000000..f0205864 --- /dev/null +++ b/tests/compiler/stdlib/array-key-exists-key-types.phpt @@ -0,0 +1,90 @@ +--TEST-- +array_key_exists preserves key conversion, diagnostics and invalid-key errors +--FILE-- + null]); + lookup(false, ['' => true]); + lookup(true, [1 => null]); + lookup('1', [1 => null]); + lookup('01', [1 => null]); + var_dump(array_key_exists(1, [1 => null]), array_key_exists('1', [1 => null])); + var_dump(array_key_exists(false, [0 => null])); + + lookup(1.5, [1 => null]); + var_dump(array_key_exists(1.5, [1 => null])); + var_dump($diagnostics === [E_DEPRECATED, E_DEPRECATED]); + $diagnostics = []; + + lookup(null, ['' => null]); + var_dump(array_key_exists(null, ['' => null])); + var_dump(count($diagnostics) === (PHP_VERSION_ID >= 80500 ? 2 : 0)); + $diagnostics = []; + + $resource = fopen(__FILE__, 'r'); + lookup($resource, [(int) $resource => null]); + var_dump($diagnostics === [E_WARNING]); + fclose($resource); + $diagnostics = []; + + lookup([], ['Array' => null]); + lookup(new \stdClass(), []); + var_dump($diagnostics === []); + + $array = [0 => null]; + $key = \std::any(false); + var_dump(key_exists($key, $array), $array->keyExists($key)); + var_dump(array_key_exists(array: $array, key: $key)); + var_dump(array_key_exists(...[$key, $array])); + restore_error_handler(); + } +} +namespace { + function main(): void + { + \KeyExistsKeys\run(); + } +} +?> +--EXPECT-- +bool(true) +bool(false) +bool(true) +bool(true) +bool(false) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +TypeError +TypeError +bool(true) +bool(true) +bool(true) +bool(true) +bool(true)