From afa493f5c0350f6372a44c501e5aa58d332700a7 Mon Sep 17 00:00:00 2001 From: Guillaume Sainthillier Date: Mon, 12 Oct 2026 12:05:24 +0200 Subject: [PATCH] [Php80] Skip loose comparisons that differ from strict on StrStartsWithRector and StrEndsWithRector --- .../Fixture/fixture_equal.php.inc | 31 ------------------- .../Fixture/skip_loose_numeric_string.php.inc | 14 +++++++++ .../Fixture/skip_loose_substr.php.inc | 15 +++++++++ .../skip_loose_substr_not_prefix.php.inc | 15 +++++++++ .../Fixture/substr_not_equal_prefix.php.inc | 31 ------------------- .../Fixture/fixture_equal.php.inc | 27 ---------------- .../Fixture/not_equal_substr.php.inc | 27 ---------------- .../Fixture/other_side_equal.php.inc | 31 ------------------- .../skip_loose_not_equal_substr.php.inc | 13 ++++++++ .../Fixture/skip_loose_numeric_string.php.inc | 14 +++++++++ .../Fixture/skip_loose_strpos.php.inc | 13 ++++++++ .../skip_loose_strpos_hardcoded.php.inc | 13 ++++++++ .../skip_loose_strpos_not_prefix.php.inc | 13 ++++++++ .../skip_loose_strpos_other_side.php.inc | 13 ++++++++ .../Fixture/skip_loose_substr.php.inc | 13 ++++++++ .../skip_loose_substr_other_side.php.inc | 15 +++++++++ .../Fixture/strpos_equal.php.inc | 27 ---------------- .../Fixture/strpos_equal_not_prefix.php.inc | 27 ---------------- .../Fixture/strpos_equal_other_side.php.inc | 27 ---------------- .../Fixture/strpos_hardcoded_equal.php.inc | 27 ---------------- .../StrposMatchAndRefactor.php | 7 ++++- .../SubstrMatchAndRefactor.php | 23 ++++++++++++++ .../Rector/Identical/StrEndsWithRector.php | 16 ++++++++++ 23 files changed, 196 insertions(+), 256 deletions(-) delete mode 100644 rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/fixture_equal.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_numeric_string.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_substr.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_substr_not_prefix.php.inc delete mode 100644 rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/substr_not_equal_prefix.php.inc delete mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/fixture_equal.php.inc delete mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/not_equal_substr.php.inc delete mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/other_side_equal.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_not_equal_substr.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_numeric_string.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_hardcoded.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_not_prefix.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_other_side.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_substr.php.inc create mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_substr_other_side.php.inc delete mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal.php.inc delete mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal_not_prefix.php.inc delete mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal_other_side.php.inc delete mode 100644 rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_hardcoded_equal.php.inc diff --git a/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/fixture_equal.php.inc b/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/fixture_equal.php.inc deleted file mode 100644 index 8c7b7b41348..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/fixture_equal.php.inc +++ /dev/null @@ -1,31 +0,0 @@ - ------ - diff --git a/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_numeric_string.php.inc b/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_numeric_string.php.inc new file mode 100644 index 00000000000..be2ba58390b --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_numeric_string.php.inc @@ -0,0 +1,14 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_substr.php.inc b/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_substr.php.inc new file mode 100644 index 00000000000..083936b940a --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_substr.php.inc @@ -0,0 +1,15 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_substr_not_prefix.php.inc b/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_substr_not_prefix.php.inc new file mode 100644 index 00000000000..b39ce0b5d33 --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/skip_loose_substr_not_prefix.php.inc @@ -0,0 +1,15 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/substr_not_equal_prefix.php.inc b/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/substr_not_equal_prefix.php.inc deleted file mode 100644 index 5453a79542c..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrEndsWithRector/Fixture/substr_not_equal_prefix.php.inc +++ /dev/null @@ -1,31 +0,0 @@ - ------ - diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/fixture_equal.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/fixture_equal.php.inc deleted file mode 100644 index 5f87efcd522..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/fixture_equal.php.inc +++ /dev/null @@ -1,27 +0,0 @@ - ------ - diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/not_equal_substr.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/not_equal_substr.php.inc deleted file mode 100644 index f48e15217a1..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/not_equal_substr.php.inc +++ /dev/null @@ -1,27 +0,0 @@ - ------ - diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/other_side_equal.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/other_side_equal.php.inc deleted file mode 100644 index 70ca84ee897..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/other_side_equal.php.inc +++ /dev/null @@ -1,31 +0,0 @@ - ------ - diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_not_equal_substr.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_not_equal_substr.php.inc new file mode 100644 index 00000000000..8b8a9ddf1f2 --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_not_equal_substr.php.inc @@ -0,0 +1,13 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_numeric_string.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_numeric_string.php.inc new file mode 100644 index 00000000000..641996d5f30 --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_numeric_string.php.inc @@ -0,0 +1,14 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos.php.inc new file mode 100644 index 00000000000..1b91f7a0681 --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos.php.inc @@ -0,0 +1,13 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_hardcoded.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_hardcoded.php.inc new file mode 100644 index 00000000000..66108c96325 --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_hardcoded.php.inc @@ -0,0 +1,13 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_not_prefix.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_not_prefix.php.inc new file mode 100644 index 00000000000..044ad29bbae --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_not_prefix.php.inc @@ -0,0 +1,13 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_other_side.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_other_side.php.inc new file mode 100644 index 00000000000..76cd1b6f3ee --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_strpos_other_side.php.inc @@ -0,0 +1,13 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_substr.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_substr.php.inc new file mode 100644 index 00000000000..e9f11181bd3 --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_substr.php.inc @@ -0,0 +1,13 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_substr_other_side.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_substr_other_side.php.inc new file mode 100644 index 00000000000..8bda0422d86 --- /dev/null +++ b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/skip_loose_substr_other_side.php.inc @@ -0,0 +1,15 @@ + diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal.php.inc deleted file mode 100644 index cd671374ff5..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal.php.inc +++ /dev/null @@ -1,27 +0,0 @@ - ------ - diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal_not_prefix.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal_not_prefix.php.inc deleted file mode 100644 index 36f6112dbee..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal_not_prefix.php.inc +++ /dev/null @@ -1,27 +0,0 @@ - ------ - diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal_other_side.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal_other_side.php.inc deleted file mode 100644 index 1e959bb197d..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_equal_other_side.php.inc +++ /dev/null @@ -1,27 +0,0 @@ - ------ - diff --git a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_hardcoded_equal.php.inc b/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_hardcoded_equal.php.inc deleted file mode 100644 index a19bf55f32e..00000000000 --- a/rules-tests/Php80/Rector/Identical/StrStartsWithRector/Fixture/strpos_hardcoded_equal.php.inc +++ /dev/null @@ -1,27 +0,0 @@ - ------ - diff --git a/rules/Php80/MatchAndRefactor/StrStartsWithMatchAndRefactor/StrposMatchAndRefactor.php b/rules/Php80/MatchAndRefactor/StrStartsWithMatchAndRefactor/StrposMatchAndRefactor.php index 55912c58310..a38f3d902d5 100644 --- a/rules/Php80/MatchAndRefactor/StrStartsWithMatchAndRefactor/StrposMatchAndRefactor.php +++ b/rules/Php80/MatchAndRefactor/StrStartsWithMatchAndRefactor/StrposMatchAndRefactor.php @@ -30,7 +30,12 @@ public function __construct( public function match(Identical|NotIdentical|Equal|NotEqual $binaryOp): ?StrStartsWith { - $isPositive = $binaryOp instanceof Identical || $binaryOp instanceof Equal; + // strpos() returns false on no match, and false == 0 + if ($binaryOp instanceof Equal || $binaryOp instanceof NotEqual) { + return null; + } + + $isPositive = $binaryOp instanceof Identical; if ($binaryOp->left instanceof FuncCall && $this->nodeNameResolver->isName($binaryOp->left, 'strpos')) { return $this->processBinaryOpLeft($binaryOp, $isPositive); diff --git a/rules/Php80/MatchAndRefactor/StrStartsWithMatchAndRefactor/SubstrMatchAndRefactor.php b/rules/Php80/MatchAndRefactor/StrStartsWithMatchAndRefactor/SubstrMatchAndRefactor.php index 344f24d4268..272b7e7d58c 100644 --- a/rules/Php80/MatchAndRefactor/StrStartsWithMatchAndRefactor/SubstrMatchAndRefactor.php +++ b/rules/Php80/MatchAndRefactor/StrStartsWithMatchAndRefactor/SubstrMatchAndRefactor.php @@ -5,6 +5,7 @@ namespace Rector\Php80\MatchAndRefactor\StrStartsWithMatchAndRefactor; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\BinaryOp\Equal; use PhpParser\Node\Expr\BinaryOp\Identical; use PhpParser\Node\Expr\BinaryOp\NotEqual; @@ -34,6 +35,10 @@ public function match(Identical|NotIdentical|Equal|NotEqual $binaryOp): ?StrStar $isPositive = $binaryOp instanceof Identical || $binaryOp instanceof Equal; if ($binaryOp->left instanceof FuncCall && $this->nodeNameResolver->isName($binaryOp->left, 'substr')) { + if (! $this->isStrictOrNonNumericStringComparison($binaryOp, $binaryOp->right)) { + return null; + } + $funcCall = $binaryOp->left; $haystack = $funcCall->getArgs()[0] @@ -43,6 +48,10 @@ public function match(Identical|NotIdentical|Equal|NotEqual $binaryOp): ?StrStar } if ($binaryOp->right instanceof FuncCall && $this->nodeNameResolver->isName($binaryOp->right, 'substr')) { + if (! $this->isStrictOrNonNumericStringComparison($binaryOp, $binaryOp->left)) { + return null; + } + $funcCall = $binaryOp->right; $haystack = $funcCall->getArgs()[0] @@ -127,4 +136,18 @@ private function isHardcodedStringWithLNumberLength(StrStartsWith $strStartsWith return $lNumberLength->value === strlen($expr->value); } + + /** + * Loose comparison of two numeric strings is numeric, e.g. '+1' == '01' + */ + private function isStrictOrNonNumericStringComparison( + Identical|NotIdentical|Equal|NotEqual $binaryOp, + Expr $comparedExpr + ): bool { + if (! $binaryOp instanceof Equal && ! $binaryOp instanceof NotEqual) { + return true; + } + + return $comparedExpr instanceof String_ && ! is_numeric($comparedExpr->value); + } } diff --git a/rules/Php80/Rector/Identical/StrEndsWithRector.php b/rules/Php80/Rector/Identical/StrEndsWithRector.php index eac7487378d..dc8338b2323 100644 --- a/rules/Php80/Rector/Identical/StrEndsWithRector.php +++ b/rules/Php80/Rector/Identical/StrEndsWithRector.php @@ -142,6 +142,10 @@ private function refactorSubstr(BinaryOp $binaryOp): FuncCall|BooleanNot|null return null; } + if (! $this->isStrictOrNonNumericStringComparison($binaryOp, $comparedNeedleExpr)) { + return null; + } + if (count($substrFuncCall->getArgs()) < 2) { return null; } @@ -252,6 +256,18 @@ private function isHardCodedLNumberAndString(Expr $substrOffset, Expr $needle): return $lNumber->value === strlen($needle->value); } + /** + * Loose comparison of two numeric strings is numeric, e.g. '+1' == '01' + */ + private function isStrictOrNonNumericStringComparison(BinaryOp $binaryOp, Expr $comparedExpr): bool + { + if (! $binaryOp instanceof Equal && ! $binaryOp instanceof NotEqual) { + return true; + } + + return $comparedExpr instanceof String_ && ! is_numeric($comparedExpr->value); + } + private function buildReturnNode(Expr $haystack, Expr $needle, bool $isPositive): FuncCall|BooleanNot { $funcCall = $this->nodeFactory->createFuncCall('str_ends_with', [$haystack, $needle]);