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
5 changes: 4 additions & 1 deletion lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -13210,7 +13210,10 @@ private function isErrorSuppressInnerExpr(Op $child): bool
|| $child instanceof Op\Expr\ArrayDimFetch
|| $child instanceof Op\Expr\Isset_
|| $child instanceof Op\Expr\Empty_
// `@$cv` materializes via Assign under silence (#13587 / #29132).
|| $child instanceof Op\Expr\UnaryPlus
|| $child instanceof Op\Expr\UnaryMinus
|| $child instanceof Op\Expr\BinaryOp
// `@$cv` materializes via Assign under silence (#13587 / #29132 / #31881).
|| $child instanceof Op\Expr\Assign;
}

Expand Down
1 change: 1 addition & 0 deletions patches/php-cfg-bare-variable-read-stmt.patch
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
+ // Bare variable reads need a real fetch op for ZEND_CHECK_UNDEFINED_VAR (#13587).
+ // Materialize into a temp under silence so `$a = @$undef` does not re-fetch the CV
+ // after END_SILENCE (#29132). UnaryPlus would coerce null→int(0); Assign preserves null.
+ // Value-consuming `@$cv` (echo / arithmetic / call arg) must use that temp (#31881).
+ if ($expr->expr instanceof Node\Expr\Variable) {
+ $tmp = new Temporary();
+ $assign = new Op\Expr\Assign(
Expand Down
23 changes: 23 additions & 0 deletions patches/php-cfg-error-suppress-cv-temp.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--- vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php
+++ vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php
@@ -1501,12 +1501,19 @@
$this->block = $block;
$result = $this->parseExprNode($expr->expr);
// Bare variable reads need a real fetch op for ZEND_CHECK_UNDEFINED_VAR (#13587).
+ // Materialize into a temp under silence so `$a = @$undef` does not re-fetch the CV
+ // after END_SILENCE (#29132). UnaryPlus would coerce null→int(0); Assign preserves null.
+ // Value-consuming `@$cv` (echo / arithmetic / call arg) must use that temp (#31881).
if ($expr->expr instanceof Node\Expr\Variable) {
- $this->block->children[] = new Op\Expr\UnaryPlus(
+ $tmp = new Temporary();
+ $assign = new Op\Expr\Assign(
+ $tmp,
$this->readVariable($result),
$attrs
);
+ $this->block->children[] = $assign;
+ $result = $assign->result;
} else {
// Evaluate reads inside the suppress region (issue #3546; Zend ZEND_COMPILE_SILENCE).
$result = $this->readVariable($result);
}
5 changes: 5 additions & 0 deletions script/apply-patches.sh
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ patch_already_applied() {
php-cfg-bare-variable-read-stmt.patch)
grep -q 'ZEND_CHECK_UNDEFINED_VAR (#13587)' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php" 2>/dev/null
;;
php-cfg-error-suppress-cv-temp.patch)
grep -q '#31881' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php" 2>/dev/null \
&& grep -q '\$assign->result' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Parser.php" 2>/dev/null
;;
php-cfg-error-suppress-simplifier.patch)
grep -q 'instanceof ErrorSuppressBlock' "$ROOT/vendor/ircmaxell/php-cfg/lib/PHPCfg/Visitor/Simplifier.php" 2>/dev/null
;;
Expand Down Expand Up @@ -6953,6 +6957,7 @@ if [[ -d "$ROOT/vendor/ircmaxell/php-cfg" ]]; then
apply_patch "$PATCH_DIR/php-cfg-nullsafe-parser.patch"
apply_patch "$PATCH_DIR/php-cfg-error-suppress-read.patch"
apply_patch "$PATCH_DIR/php-cfg-bare-variable-read-stmt.patch"
apply_patch "$PATCH_DIR/php-cfg-error-suppress-cv-temp.patch"
apply_patch "$PATCH_DIR/php-cfg-error-suppress-simplifier.patch"
apply_patch "$PATCH_DIR/php-cfg-simplifier-call-unpack.patch" || true
apply_patch "$PATCH_DIR/php-cfg-strict-types.patch"
Expand Down
31 changes: 31 additions & 0 deletions test/compliance/cases/language/at_silence_value_undef.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Language: value-consuming @$undef is silenced; later bare read still warns (#31881)
--FILE--
<?php
error_reporting(E_ALL);
set_error_handler(function (int $no, string $str): bool {
if (0 !== error_reporting()) {
echo 'LIVE:', $str, "\n";
}
return true;
});

echo var_export(@$undef_echo, true), "\n";
echo @$undef_plus + 1, "\n";
echo strlen(@$undef_call), "\n";
$a = @$undef_assign;
var_dump($a);
@$undef_bare;
print @$undef_print;
echo "print-ok\n";
echo $undef_echo;
echo "done\n";
--EXPECT--
NULL
1
LIVE:strlen(): Passing null to parameter #1 ($string) of type string is deprecated
0
NULL
print-ok
LIVE:Undefined variable $undef_echo
done
31 changes: 31 additions & 0 deletions test/unit/ErrorControlOperatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public static function providePHPTests(): \Generator
__DIR__.'/../compliance/cases/language/at_silence_assign_undef_rhs.phpt',
'at_silence_assign_undef_rhs.phpt'
);
yield 'at_silence_value_undef.phpt' => self::parsePHPT(
__DIR__.'/../compliance/cases/language/at_silence_value_undef.phpt',
'at_silence_value_undef.phpt'
);
}

/** Issue #29132 — `$a = @$undef` must not print Undefined variable on stderr. */
Expand All @@ -52,4 +56,31 @@ public function testAssignUndefRhsSilenceLeavesStderrEmpty(): void
$this->assertSame("ok\n", $stdout);
$this->assertStringNotContainsString('Undefined variable', $stderr);
}

/** Issue #31881 — echo / arithmetic / call-arg `@$undef` must not print Undefined variable. */
public function testValueConsumingUndefSilenceLeavesStderrEmpty(): void
{
$code = <<<'PHP'
<?php
error_reporting(E_ALL);
echo @$undef_echo_31881;
echo @$undef_plus_31881 + 1;
strlen(@$undef_call_31881);
echo "ok\n";
PHP;
$repoRoot = dirname(__DIR__, 2);
$env = [];
foreach (array_merge($_ENV, $_SERVER) as $key => $value) {
if (is_string($value)) {
$env[$key] = $value;
}
}
self::applyLlvmToolchainEnv($env);
unset($env['PHP_COMPILER_SKIP_LLVM_PRELOAD']);
$cmd = array_merge(self::llvmEnvPrefix(), $this->phpCommand(), [$this->BIN]);
[$stdout, $exitCode, $stderr] = self::runVmSubprocess($cmd, $repoRoot, $env, $code, __FUNCTION__);
$this->assertSame(0, $exitCode, $stderr);
$this->assertStringNotContainsString('Undefined variable', $stderr);
$this->assertStringContainsString('ok', $stdout);
}
}
Loading