Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
2 changes: 2 additions & 0 deletions config/set/downgrade-php80.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Rector\DowngradePhp80\Rector\Expression\DowngradeMatchToSwitchRector;
use Rector\DowngradePhp80\Rector\Expression\DowngradeThrowExprRector;
use Rector\DowngradePhp80\Rector\FuncCall\DowngradeArrayFilterNullableCallbackRector;
use Rector\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector;
use Rector\DowngradePhp80\Rector\FuncCall\DowngradeNumberFormatNoFourthArgRector;
use Rector\DowngradePhp80\Rector\FuncCall\DowngradeStrContainsRector;
use Rector\DowngradePhp80\Rector\FuncCall\DowngradeStrEndsWithRector;
Expand Down Expand Up @@ -70,6 +71,7 @@
DowngradePropertyPromotionRector::class,
DowngradeNonCapturingCatchesRector::class,
DowngradeStrContainsRector::class,
DowngradeMbStrContainsRector::class,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unregister this, there is no "mb_str_contains", only str_contains, that means that utilize strpos can be on purpose.

let user manually use if needed, dealing with user that may don't have mbstring extension or use strpos on purpose :)

user that use mb_ functions know why they are using the mb_ functions :)

DowngradeMatchToSwitchRector::class,
DowngradeClassOnObjectToGetClassRector::class,
DowngradeArbitraryExpressionsSupportRector::class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector;

use Iterator;
use PHPUnit\Framework\Attributes\DataProvider;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;

final class DowngradeMbStrContainsRectorTest extends AbstractRectorTestCase
{
#[DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public static function provideData(): Iterator
{
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Rector\Tests\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector\Fixture;

final class NoStrContains
{
public function run()
{
return ! str_contains('abc', 'b');
}
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector\Fixture;

final class NoStrContains
{
public function run()
{
return mb_strpos('abc', 'b') === false;
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Rector\Tests\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector\Fixture;

final class StrContains
{
public function run()
{
str_contains('abc', 'b');
}
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector\Fixture;

final class StrContains
{
public function run()
{
mb_strpos('abc', 'b') !== false;
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Rector\Tests\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector\Fixture;

final class StrContainsPhpWithMixed
{
public function run($haystack, $needle)
{
str_contains($haystack, 'ab');
str_contains('abc', $needle);
}
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector\Fixture;

final class StrContainsPhpWithMixed
{
public function run($haystack, $needle)
{
mb_strpos($haystack, 'ab') !== false;
mb_strpos('abc', $needle) !== false;
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(DowngradeMbStrContainsRector::class);
};
103 changes: 103 additions & 0 deletions rules/DowngradePhp80/Rector/FuncCall/DowngradeMbStrContainsRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace Rector\DowngradePhp80\Rector\FuncCall;

use PhpParser\Node;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
use PhpParser\Node\Expr\BooleanNot;
use PhpParser\Node\Expr\FuncCall;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @changelog https://wiki.php.net/rfc/str_contains
*
* @see \Rector\Tests\DowngradePhp80\Rector\FuncCall\DowngradeMbStrContainsRector\DowngradeMbStrContainsRectorTest
*/
final class DowngradeMbStrContainsRector extends AbstractRector
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer to rename to something like: DowngradeStrContainsWithMultibyteNeedleRector since there is no mb_str_contains, make clearer it actually use when needle has multibyte.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

{
public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Replace str_contains() with mb_strpos() !== false',
[
new CodeSample(
<<<'CODE_SAMPLE'
class SomeClass
{
public function run()
{
return str_contains('abc', 'a');
Copy link
Copy Markdown
Member

@samsonasik samsonasik Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only when needle has multibyte value as well:

Suggested change
return str_contains('abc', 'a');
return str_contains('😊abc', '😊a');

}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeClass
{
public function run()
{
return mb_strpos('abc', 'a') !== false;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only when needle has multibyte value as well:

Suggested change
return mb_strpos('abc', 'a') !== false;
return mb_strpos('😊abc', '😊a') !== false;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok

}
}
CODE_SAMPLE
),
]
);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [FuncCall::class, BooleanNot::class];
}

/**
* @param FuncCall|BooleanNot $node
* @return Identical|NotIdentical|null The refactored node.
*/
public function refactor(Node $node): Identical | NotIdentical | null
{
$funcCall = $this->matchStrContainsOrNotStrContains($node);

if (! $funcCall instanceof FuncCall) {
return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

early skip first class callable here:

if ($funcCall->isFirstClassCallable()) {
    return null;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (!$funcCall->isFirstClassCallable()) {
  return null;
}

Correct ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, should be below if (! $funcCall instanceof FuncCall) {:

        if (! $funcCall instanceof FuncCall) {
            return null;
        }

        if ($funcCall->isFirstClassCallable()) {
            return null;
        }

$args = $funcCall->getArgs();
if (count($args) < 2) {
return null;
}

$haystack = $args[0]->value;
$needle = $args[1]->value;

$funcCall = $this->nodeFactory->createFuncCall('mb_strpos', [$haystack, $needle]);

if ($node instanceof BooleanNot) {
return new Identical($funcCall, $this->nodeFactory->createFalse());
}

return new NotIdentical($funcCall, $this->nodeFactory->createFalse());
}

private function matchStrContainsOrNotStrContains(FuncCall | BooleanNot $expr): ?FuncCall
{
$expr = ($expr instanceof BooleanNot) ? $expr->expr : $expr;
if (! $expr instanceof FuncCall) {
return null;
}

if (! $this->isName($expr, 'str_contains')) {
return null;
}

return $expr;
}
}