Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12525,6 +12525,27 @@ private function matchInlineCallArgProducer(
return $producers[$mappedIndex] ?? null;
}
if ($producerCount === $argCount) {
// filter_var('x', FILTER_*, ['options' => ['regexp' => '/a/']]) — ConstFetch + nested Array_ (#12007).
$leadingConstNested = $this->splitLeadingConstFetchWithNestedArrayLiteralChain($producers);
if (null !== $leadingConstNested) {
[$constFetch, $arrayChain] = $leadingConstNested;
$arrayArgIndex = $argCount - 1;
if ($argIndex === $arrayArgIndex) {
return $arrayChain[\count($arrayChain) - 1];
}
$constArgIndex = null;
for ($i = $arrayArgIndex - 1; $i >= 0; --$i) {
if (!$this->isEmbeddedCallLiteralArg($callArgs[$i] ?? null)) {
$constArgIndex = $i;
break;
}
}
if ($argIndex === $constArgIndex) {
return $constFetch;
}

return null;
}
// php-cfg `f(g(), h())` hoists sibling FuncCall producers with dead arg temps (#9463, #10917).
if ($argIndex < $producerCount) {
$allSiblingFuncCalls = true;
Expand Down Expand Up @@ -13462,6 +13483,33 @@ private function splitNestedArrayLiteralChainWithTrailingProducers(array $produc
return [$arrayChain, $trailing];
}

/**
* ConstFetch prelude before nested inline Array_ call arg (#12007, filter_var + options array).
*
* e.g. filter_var('abc', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^a/']])
* — producers [ConstFetch, inner Array_, outer Array_].
*
* @param list<Op\Expr> $producers
*
* @return array{0: Op\Expr\ConstFetch, 1: list<Op\Expr\Array_>}|null
*/
private function splitLeadingConstFetchWithNestedArrayLiteralChain(array $producers): ?array
{
$first = $producers[0] ?? null;
if (!$first instanceof Op\Expr\ConstFetch) {
return null;
}
$rest = array_slice($producers, 1);
if ([] === $rest || !$this->producersAreNestedArrayLiteralChain($rest)) {
return null;
}
if (!$this->arrayProducersFormNestedChain($rest)) {
return null;
}

return [$first, $rest];
}

/**
* php-cfg hoists chained assignment before a call with a dead arg temp (#6758, #9405).
*
Expand Down
18 changes: 18 additions & 0 deletions test/repro/maintainer_gap_filter_validate_regexp_options.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);

$r = filter_var('abc', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^a/']]);
if ('abc' !== $r) {
echo 'fail: regexp expected abc, got ';
var_export($r);
exit(1);
}

$r2 = filter_var('x', FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE]);
if (null !== $r2) {
echo 'fail: int null on failure expected null, got ';
var_export($r2);
exit(1);
}

echo "ok\n";
11 changes: 11 additions & 0 deletions test/repro/maintainer_gap_filter_var_int_null_on_failure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);

$r = filter_var('x', FILTER_VALIDATE_INT, ['flags' => FILTER_NULL_ON_FAILURE]);
if (null !== $r) {
echo 'fail: expected null, got ';
var_export($r);
exit(1);
}

echo "ok\n";
50 changes: 50 additions & 0 deletions test/unit/InlineCallArgProducerSlotTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1745,6 +1745,56 @@ public function testPasswordHashConstFetchAndArrayOptionSlots(): void
self::assertSame($arraySlot, $sendSlots[2] ?? null, 'arg sends='.json_encode($sendSlots));
}

/** Issue #12007 — filter_var() ConstFetch + nested options Array_ map to filter and options slots. */
public function testFilterVarConstFetchAndNestedOptionsArraySlots(): void
{
$code = <<<'PHP'
<?php
filter_var('abc', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^a/']]);
PHP;
$runtime = new Runtime();
$block = $runtime->parseAndCompile($code, 'filter_var_nested_options.php');

$constSlot = null;
$outerArraySlot = null;
$sendSlots = [];
foreach ($block->opCodes as $op) {
if (OpCode::TYPE_CONST_FETCH === $op->type && null === $constSlot) {
$constSlot = $op->arg1;
}
if (OpCode::TYPE_INIT_ARRAY === $op->type) {
$outerArraySlot = $op->arg1;
}
if (OpCode::TYPE_ARG_SEND === $op->type) {
$sendSlots[] = $op->arg1;
}
}

self::assertNotNull($constSlot);
self::assertNotNull($outerArraySlot);
self::assertCount(3, $sendSlots, 'filter_var arg sends='.json_encode($sendSlots));
self::assertSame($constSlot, $sendSlots[1] ?? null, 'arg sends='.json_encode($sendSlots));
self::assertSame($outerArraySlot, $sendSlots[2] ?? null, 'arg sends='.json_encode($sendSlots));
}

/** Issue #12007 — filter_var() nested inline options runtime parity with Zend. */
public function testFilterVarNestedOptionsArrayRuntime(): void
{
$code = <<<'PHP'
<?php
declare(strict_types=1);

$r = filter_var('abc', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '/^a/']]);
echo $r === 'abc' ? "ok\n" : "bad\n";
PHP;
$runtime = new Runtime();
$block = $runtime->parseAndCompile($code, 'filter_var_nested_options_runtime.php');
ob_start();
$runtime->run($block);
$out = ob_get_clean();
self::assertSame("ok\n", $out);
}

/** Issue #10566 — count([nested inline], COUNT_RECURSIVE) wires outer Array_ + mode const. */
public function testCountNestedInlineLiteralRecursiveUsesArrayAndModeSlots(): void
{
Expand Down