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
74 changes: 62 additions & 12 deletions lib/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4042,7 +4042,7 @@ private function compileEchoWithEmbeddedCoalesce(Op $op, Block $block, array $op
}
$parts[] = $replaced;
}
$concat = new Op\Expr\ConcatList($parts);
$concat = new Op\Expr\ConcatList($parts, $flattened->getAttributes());
$concat->result = $flattened->result;
$this->compileOp($concat, $block);
$var = $this->compileOperand($concat->result, $block, true);
Expand Down Expand Up @@ -5495,7 +5495,7 @@ private function flattenBinaryConcatFromBlockOps(array $ops, int $echoIndex, Ope
return null;
}
$parts = array_reverse($parts);
$list = new Op\Expr\ConcatList($parts);
$list = new Op\Expr\ConcatList($parts, $outer->getAttributes());
$list->result = $outer->result;

return $list;
Expand Down Expand Up @@ -5528,7 +5528,7 @@ private function flattenBinaryConcatToConcatList(?Operand $operand): ?Op\Expr\Co
return null;
}
$parts = array_reverse($parts);
$list = new Op\Expr\ConcatList($parts);
$list = new Op\Expr\ConcatList($parts, $topConcat->getAttributes());
$list->result = $topConcat->result;

return $list;
Expand Down Expand Up @@ -5763,15 +5763,17 @@ private function materializeConcatListCoalesceParts(Op\Expr\ConcatList $concat,
$readSlot = $this->compileOperand($part, $block, true);
$fresh = new Operand\Temporary();
$writeSlot = $block->forceFreshVarSlot($fresh);
$block->addOpCode(new OpCode(
$assignOp = new OpCode(
OpCode::TYPE_ASSIGN,
$writeSlot,
$writeSlot,
$readSlot
));
);
$this->assignConcatListSourceMetadata($assignOp, $concat);
$block->addOpCode($assignOp);
$parts[] = $fresh;
}
$materialized = new Op\Expr\ConcatList($parts);
$materialized = new Op\Expr\ConcatList($parts, $concat->getAttributes());
$materialized->result = $concat->result;

return $materialized;
Expand Down Expand Up @@ -5826,6 +5828,54 @@ private function compileConcatListPart(Operand $part, Block $block): int
return $this->compileOperand($part, $block, true);
}

/**
* CONCAT/CAST_STRING from encapsed ConcatList must carry the user site so
* Undefined variable warnings do not inherit the prior statement's opline (#32034).
*
* php-src: Zend/zend_compile.c zend_compile_encapsed_string — FETCH_R lineno is the
* interpolated expression, not the previous statement.
*/
private function addConcatListOpCode(Block $block, OpCode $opcode, Op\Expr\ConcatList $concat): void
{
$this->assignConcatListSourceMetadata($opcode, $concat);
$block->addOpCode($opcode);
}

private function assignConcatListSourceMetadata(OpCode $opcode, Op\Expr\ConcatList $concat): void
{
$this->assignSourceMetadata($opcode, $concat);
$line = $this->concatListWarningLine($concat);
if ($line <= 0) {
return;
}
$loc = $opcode->sourceLocation;
if (null !== $loc && $loc->startLine === $line) {
return;
}
$opcode->sourceLocation = new SourceLocation(
$loc?->docComment,
$line,
$loc?->endLine ?? max(0, (int) $concat->getAttribute('endLine', 0)),
$loc?->filename ?? (string) $concat->getAttribute('filename', '')
);
}

/**
* Heredoc ConcatList startLine is the `<<<LABEL` opener; Zend FETCH_R cites the body
* (php-parser String_::KIND_HEREDOC === 3, #32034).
*/
private function concatListWarningLine(Op\Expr\ConcatList $concat): int
{
$start = max(0, $concat->getLine());
$end = max(0, (int) $concat->getAttribute('endLine', 0));
$kind = (int) $concat->getAttribute('kind', 0);
if (3 === $kind && $end > $start && $start > 0) {
return $start + 1;
}

return $start;
}

/** Concat destination must not alias an active catch variable slot (#17384). */
private function concatResultSlotAliasesCatchVar(int $slot): bool
{
Expand Down Expand Up @@ -12933,20 +12983,20 @@ protected function compileOp(Op $op, Block $block) {
$empty = new Operand\Literal('');
$empty->type = Type::string();
$emptySlot = $this->compileOperand($empty, $block, true);
$block->addOpCode(new OpCode(
$this->addConcatListOpCode($block, new OpCode(
OpCode::TYPE_ASSIGN,
$return,
$return,
$emptySlot
));
), $op);
} elseif (1 === $total) {
// Zend string context for a lone encapsed variable (#4785) — not a plain assign.
$part = $this->compileConcatListPart($op->list[0], $block);
$block->addOpCode(new OpCode(
$this->addConcatListOpCode($block, new OpCode(
OpCode::TYPE_CAST_STRING,
$return,
$part
));
), $op);
} else {
// Encapsed ConcatList used to emit in-place CONCAT($return, $return, $right).
// Reusing one dead-temp slot for every link intermittently heap-corrupts under
Expand All @@ -12959,12 +13009,12 @@ protected function compileOp(Op $op, Block $block) {
$dest = $isLast
? $return
: $block->forceFreshVarSlot(new Temporary());
$block->addOpCode(new OpCode(
$this->addConcatListOpCode($block, new OpCode(
OpCode::TYPE_CONCAT,
$dest,
$acc,
$right
));
), $op);
$acc = $dest;
}
}
Expand Down
4 changes: 4 additions & 0 deletions lib/JIT.php
Original file line number Diff line number Diff line change
Expand Up @@ -8135,6 +8135,10 @@ private function compileBlockInternal(

for ($i = $startIndex, $length = null !== $limit ? $limit : count($block->opCodes); $i < $length; ++$i) {
$op = $block->opCodes[$i];
// Current opline for runtime warnings (encapsed CONCAT FETCH_R, #32034).
if (null !== $op->sourceLocation && $op->sourceLocation->startLine > 0) {
$this->context->callSiteLine = $op->sourceLocation->startLine;
}
if (
null !== $block->func
&& '{main}' === $block->func->name
Expand Down
2 changes: 2 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@
<file>./test/compliance/DynamicPropAssignWithGetNoSet31949JITTest.php</file>
<file>./test/compliance/ListDestructUndefKeyLine31994VMTest.php</file>
<file>./test/compliance/ListDestructUndefKeyLine31994JITTest.php</file>
<file>./test/compliance/EncapsedUndefVarWarningLine32034VMTest.php</file>
<file>./test/compliance/EncapsedUndefVarWarningLine32034JITTest.php</file>
<file>./test/compliance/VarDumpUninitTypedProperty31147VMTest.php</file>
<file>./test/compliance/VarDumpUninitTypedProperty31147JITTest.php</file>
<file>./test/compliance/InOperatorPhpSrcReject31158VMTest.php</file>
Expand Down
32 changes: 32 additions & 0 deletions test/compliance/EncapsedUndefVarWarningLine32034JITTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

require_once __DIR__.'/../BaseTest.php';

/**
* JIT: encapsed/heredoc Undefined variable Warning cites user site (#32034).
*
* Dedicated provider — path-slash data-set names break --filter on full JITTest.
*
* @group llvm
*/
final class EncapsedUndefVarWarningLine32034JITTest extends BaseTest
{
protected static string $DIR = __DIR__;

public static function providePHPTests(): \Generator
{
yield 'encapsed_undef_var_warning_line.phpt' => self::parsePHPT(
__DIR__.'/cases/language/encapsed_undef_var_warning_line.phpt',
'encapsed_undef_var_warning_line.phpt'
);
}

public function setUp(): void
{
$this->BIN = realpath(__DIR__.'/../../bin/jit.php');
}
}
30 changes: 30 additions & 0 deletions test/compliance/EncapsedUndefVarWarningLine32034VMTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

require_once __DIR__.'/../BaseTest.php';

/**
* VM: encapsed/heredoc Undefined variable Warning cites user site (#32034).
*
* Dedicated provider — path-slash data-set names break --filter on full VMTest.
*/
final class EncapsedUndefVarWarningLine32034VMTest extends BaseTest
{
protected static string $DIR = __DIR__;

public static function providePHPTests(): \Generator
{
yield 'encapsed_undef_var_warning_line.phpt' => self::parsePHPT(
__DIR__.'/cases/language/encapsed_undef_var_warning_line.phpt',
'encapsed_undef_var_warning_line.phpt'
);
}

public function setUp(): void
{
$this->BIN = realpath(__DIR__.'/../../bin/vm.php');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
--TEST--
Language: encapsed/heredoc Undefined variable Warning cites user site (#32034, Zend/zend_compile.c)
--FILE--
<?php
function warn_line(int $errno, string $message, string $file, int $line): bool
{
if (E_WARNING === $errno) {
echo 'W:', $message, '@', $line, "\n";
}

return true;
}
set_error_handler('warn_line');

echo "x=$missing_dq\n";
$s = "a$missing_asg b";
$s = "a{$missing_brace} b";
echo "${missing_dollar}\n";
$h = <<<EOT
a$missing_heredoc
EOT;
echo $missing_plain;
echo "done\n";
--EXPECT--
W:Undefined variable $missing_dq@12
x=
W:Undefined variable $missing_asg@13
W:Undefined variable $missing_brace@14
W:Undefined variable $missing_dollar@15

W:Undefined variable $missing_heredoc@17
W:Undefined variable $missing_plain@19
done
6 changes: 6 additions & 0 deletions test/repro/maintainer_gap_undef_var_encapsed_line.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php
error_reporting(E_ALL);
echo "x=$missing\n";
$s = "a$missing b";
$s = "a{$missing} b";
echo "${missing}\n";
5 changes: 5 additions & 0 deletions test/repro/maintainer_gap_undef_var_heredoc_line.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
error_reporting(E_ALL);
$s = <<<EOT
a$missing
EOT;
3 changes: 3 additions & 0 deletions test/repro/maintainer_gap_undef_var_line.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php
error_reporting(E_ALL);
echo $missing;
40 changes: 40 additions & 0 deletions test/unit/UndefinedVariableWarningLineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,46 @@ public function testBinaryExprUndefinedVariableWarningIncludesLineSuffix(): void
);
}

public function testEncapsedUndefinedVariableWarningCitesExpressionLine(): void
{
$script = $this->repoRoot.'/test/repro/maintainer_gap_undef_var_encapsed_line.php';
[, $stderr] = $this->runVmScript($script, 0);
$this->assertMatchesRegularExpression(
'/Undefined variable \$missing in .+maintainer_gap_undef_var_encapsed_line\.php on line 3/m',
$stderr
);
$this->assertMatchesRegularExpression(
'/Undefined variable \$missing in .+maintainer_gap_undef_var_encapsed_line\.php on line 4/m',
$stderr
);
$this->assertMatchesRegularExpression(
'/Undefined variable \$missing in .+maintainer_gap_undef_var_encapsed_line\.php on line 5/m',
$stderr
);
$this->assertMatchesRegularExpression(
'/Undefined variable \$missing in .+maintainer_gap_undef_var_encapsed_line\.php on line 6/m',
$stderr
);
$this->assertDoesNotMatchRegularExpression(
'/Undefined variable \$missing in .+maintainer_gap_undef_var_encapsed_line\.php on line 2/m',
$stderr
);
}

public function testHeredocUndefinedVariableWarningCitesBodyLine(): void
{
$script = $this->repoRoot.'/test/repro/maintainer_gap_undef_var_heredoc_line.php';
[, $stderr] = $this->runVmScript($script, 0);
$this->assertMatchesRegularExpression(
'/Undefined variable \$missing in .+maintainer_gap_undef_var_heredoc_line\.php on line 4\s*$/m',
$stderr
);
$this->assertDoesNotMatchRegularExpression(
'/Undefined variable \$missing in .+maintainer_gap_undef_var_heredoc_line\.php on line 2/m',
$stderr
);
}

/**
* @return array{0: string, 1: string}
*/
Expand Down
Loading