From 5af5cbb837cb1fc0ad9ba37d5a82b690010dedf3 Mon Sep 17 00:00:00 2001 From: NathanFreeman <1056159381@qq.com> Date: Mon, 13 Jul 2026 22:26:29 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E5=9C=A8=E5=AD=97=E7=AC=A6=E4=B8=B2?= =?UTF-8?q?=E6=8B=BC=E6=8E=A5=E4=B8=AD=EF=BC=8C=E9=99=A4=E4=BA=86=E7=AC=AC?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E5=85=83=E7=B4=A0=E4=B9=8B=E5=A4=96=EF=BC=8C?= =?UTF-8?q?=E5=89=A9=E4=B8=8B=E7=9A=84=E5=AD=97=E7=AC=A6=E4=B8=B2=E5=A6=82?= =?UTF-8?q?=E6=9E=9C=E5=87=BA=E7=8E=B0=E7=A9=BA=E5=AD=97=E7=AC=A6=E4=B8=B2?= =?UTF-8?q?=EF=BC=8C=E5=BF=BD=E7=95=A5=E8=BF=99=E4=B8=AA=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Parser/BinaryOpTrait.php | 15 +++++++++++---- tests/compiler/concat_empty.phpt | 12 ++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 tests/compiler/concat_empty.phpt diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index e57f987c..8d4d32b1 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -264,10 +264,17 @@ trait BinaryOpTrait $this->flattenConcatExpr($expr, $items); $argList = $prefixExpressions; - foreach ($items as $item) { - $type = $this->detectTypeOfExpr($item); - $parsed = $this->parseExprAsValue($item); - $argList[] = $this->prepareConcatOperand($parsed, $type); + foreach ($items as $index => $item) { + // 在字符串拼接中,除了第一个元素之外,剩下的字符串如果出现空字符串,忽略这个字符串 + if ($index > 0 && $item->value == '') { + continue; + } + + $argList[] = $this->prepareConcatOperand($this->parseExprAsValue($item), $this->detectTypeOfExpr($item)); + } + + if (sizeof($argList) == 1) { + return $argList[0]; } return Symbol::concat() . '({' . implode(', ', $argList) . '})'; diff --git a/tests/compiler/concat_empty.phpt b/tests/compiler/concat_empty.phpt new file mode 100644 index 00000000..d759d8e1 --- /dev/null +++ b/tests/compiler/concat_empty.phpt @@ -0,0 +1,12 @@ +--TEST-- +Concat empty strings +--FILE-- + +--EXPECT-- +string(2) "1a" +string(1) "1" -- 2.34.1 From 20b54e3961e1b61ce15878b2989404c7c34f1a33 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 14 Jul 2026 12:15:13 +0800 Subject: [PATCH 2/3] fix(parser): safely omit empty concat literals --- src/Parser/BinaryOpTrait.php | 16 ++++++++-------- tests/compiler/concat_empty.phpt | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Parser/BinaryOpTrait.php b/src/Parser/BinaryOpTrait.php index 8d4d32b1..83332cb4 100644 --- a/src/Parser/BinaryOpTrait.php +++ b/src/Parser/BinaryOpTrait.php @@ -264,17 +264,17 @@ trait BinaryOpTrait $this->flattenConcatExpr($expr, $items); $argList = $prefixExpressions; - foreach ($items as $index => $item) { - // 在字符串拼接中,除了第一个元素之外,剩下的字符串如果出现空字符串,忽略这个字符串 - if ($index > 0 && $item->value == '') { + foreach ($items as $item) { + // Keep one operand so concat still performs PHP string coercion. + // Prefix expressions are operands too (for example, the left-hand + // value of `.=`), so an empty RHS literal can be omitted there. + if ($argList !== [] && $this->isScalarString($item) && $item->value === '') { continue; } - $argList[] = $this->prepareConcatOperand($this->parseExprAsValue($item), $this->detectTypeOfExpr($item)); - } - - if (sizeof($argList) == 1) { - return $argList[0]; + $type = $this->detectTypeOfExpr($item); + $parsed = $this->parseExprAsValue($item); + $argList[] = $this->prepareConcatOperand($parsed, $type); } return Symbol::concat() . '({' . implode(', ', $argList) . '})'; diff --git a/tests/compiler/concat_empty.phpt b/tests/compiler/concat_empty.phpt index d759d8e1..a0aeebf1 100644 --- a/tests/compiler/concat_empty.phpt +++ b/tests/compiler/concat_empty.phpt @@ -5,8 +5,29 @@ Concat empty strings function main() { var_dump('' . 1 . '' . 'a' . ''); var_dump('' . 1); + var_dump(1 . ''); + var_dump(false . ''); + var_dump('' . '' . ''); + + $value = 'value'; + var_dump('' . $value . ''); + var_dump('' . get_value() . ''); + + $number = 1; + $number .= ''; + var_dump($number); +} + +function get_value(): string { + return 'called'; } ?> --EXPECT-- string(2) "1a" string(1) "1" +string(1) "1" +string(0) "" +string(0) "" +string(5) "value" +string(6) "called" +string(1) "1" -- 2.34.1 From 39bec62e15e37e0fe173fff4982840dc37c6f68f Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 14 Jul 2026 12:38:24 +0800 Subject: [PATCH 3/3] fix(compiler): infer concat expressions as strings --- phpunit/src/SsaAnalysisTest.php | 9 +++++++++ src/CompilerBase.php | 3 +++ 2 files changed, 12 insertions(+) diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index 89396f87..e0739242 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -1263,6 +1263,15 @@ class SsaAnalysisTest extends TestCase } } + public function testDetectTypeOfConcatExpressions(): void + { + $concat = new Expr\BinaryOp\Concat(new Scalar\LNumber(1), new Scalar\String_('')); + $concatAssign = new Expr\AssignOp\Concat(new Expr\Variable('value'), new Scalar\LNumber(2)); + + $this->assertSame(Type::STR, $this->invoke('detectTypeOfExpr', $concat)); + $this->assertSame(Type::STR, $this->invoke('detectTypeOfExpr', $concatAssign)); + } + public function testDetectSsaDefTypeNull(): void { $ssaVar = new SsaVar(1, 'x'); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 2d8f90dd..36da7790 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -2299,6 +2299,9 @@ class CompilerBase implements PropertyAccessContext case 'Expr_Array': case 'Expr_Cast_Array': return Type::ARRAY; + case 'Expr_BinaryOp_Concat': + case 'Expr_AssignOp_Concat': + return Type::STR; case 'Expr_BinaryOp_Plus': case 'Expr_BinaryOp_Minus': case 'Expr_BinaryOp_Mul': -- 2.34.1