diff --git a/lib/Compiler.php b/lib/Compiler.php index 7be1de26a85..17ad072e481 100644 --- a/lib/Compiler.php +++ b/lib/Compiler.php @@ -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); @@ -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; @@ -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; @@ -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; @@ -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 `<<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 { @@ -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 @@ -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; } } diff --git a/lib/JIT.php b/lib/JIT.php index 98cbc7c66be..090977c389e 100644 --- a/lib/JIT.php +++ b/lib/JIT.php @@ -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 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0b5d52cdc0b..923c2c2ca90 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -280,6 +280,8 @@ ./test/compliance/DynamicPropAssignWithGetNoSet31949JITTest.php ./test/compliance/ListDestructUndefKeyLine31994VMTest.php ./test/compliance/ListDestructUndefKeyLine31994JITTest.php + ./test/compliance/EncapsedUndefVarWarningLine32034VMTest.php + ./test/compliance/EncapsedUndefVarWarningLine32034JITTest.php ./test/compliance/VarDumpUninitTypedProperty31147VMTest.php ./test/compliance/VarDumpUninitTypedProperty31147JITTest.php ./test/compliance/InOperatorPhpSrcReject31158VMTest.php diff --git a/test/compliance/EncapsedUndefVarWarningLine32034JITTest.php b/test/compliance/EncapsedUndefVarWarningLine32034JITTest.php new file mode 100644 index 00000000000..dfc797a19b0 --- /dev/null +++ b/test/compliance/EncapsedUndefVarWarningLine32034JITTest.php @@ -0,0 +1,32 @@ + 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'); + } +} diff --git a/test/compliance/EncapsedUndefVarWarningLine32034VMTest.php b/test/compliance/EncapsedUndefVarWarningLine32034VMTest.php new file mode 100644 index 00000000000..4a220f27fec --- /dev/null +++ b/test/compliance/EncapsedUndefVarWarningLine32034VMTest.php @@ -0,0 +1,30 @@ + 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'); + } +} diff --git a/test/compliance/cases/language/encapsed_undef_var_warning_line.phpt b/test/compliance/cases/language/encapsed_undef_var_warning_line.phpt new file mode 100644 index 00000000000..59049653d9b --- /dev/null +++ b/test/compliance/cases/language/encapsed_undef_var_warning_line.phpt @@ -0,0 +1,33 @@ +--TEST-- +Language: encapsed/heredoc Undefined variable Warning cites user site (#32034, Zend/zend_compile.c) +--FILE-- +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} */