pestphp/pest-plugin-mutate

Mutates your code to find untested cases

Fund package maintenance!
gehrisandro

v2.0.0-beta.5 2024-08-05 17:07 UTC

README

OpenAI PHP

GitHub Workflow Status (main) Total Downloads Latest Version License

This repository contains the Pest Plugin Mutate.

This plugin is currently under development.

But you can still use it safely, as it does not affect your application, when not explicitly activated.

Roadmap

We are going to release new beta versions on a regular basis.

A stable version will be released at Laracon US by the end of August 2024.

To stay but to date, star the repository and follow it closely.

Another option is to follow Sandro Gehri on Twitter/X or LinkedIn, where I will post tips and tricks, and updates about the ongoing development.

If you have any question not covered in the docs or need help. Checkout the discussions section on GitHub.

Mutation Testing

Note: Before you can start using mutation testing, you need to have a successfully running test suite.

Source code: Pest Plugin Mutate

To start using Pest's Mutation plugin, you need to require the plugin via Composer.

composer require pestphp/pest-plugin-mutate --dev

Run mutation testing

Run from CLI

You can run the mutation testing from the CLI providing the --mutate option.

vendor/bin/pest --mutate

When you are working with a larger codebase, checkout the performance section first, otherwise you will not have satisfying experience.

By default, it uses the default configuration (if available) or you can use a different configuration by providing its name.

vendor/bin/pest --mutate="arithmetic only"

You can set or overwrite all options available.

vendor/bin/pest --mutate --path=src

Additionally, you can make use of most of the options available in Pest.
For example this would only run mutation tests for code covered by tests in the "unit" group.

vendor/bin/pest --mutate --covered-only --group=unit

mutate()

Another powerful technique is to call the mutate() function directly in your test file. This automatically start mutation testing ones you run vendor/bin/pest. Additionally it limits the test run, to the tests in this file.

This function is intended to be used in your daily development workflow to establish a mutation testing practice right when you are implementing or modifying a feature.

By default, it inherits the default configuration. You can change this by providing an alternative configuration name.

In conjunction with the next release of Pest, it will be possible to append the mutate() function direct to an individual test case or a describe block.

mutate();

test('sum', function () {
  $result = sum(1, 2);

  expect($result)->toBe(3);
})

Executing the ./vendor/bin/pest command will now automatically run mutation testing. It is not necessary to provide the --mutate option.

You can append options after calling mutate().

->mutate()
  ->path('src/functions.php')

test('sum', function () {
  $result = sum(1, 2);

  expect($result)->toBe(3);
});

Configuration

You can globally configure mutation testing in you Pest.php file.

mutate()
    ->paths('src');

For all the available options see Options section.

Alternative configurations

You can create multiple mutation testing configurations.

use Pest\Mutate\Mutators;

mutate('arithmetic only') // 'default' if not provided
    ->paths('src')
    ->mutators(Mutators::SET_ARITHMETIC);

And you can inherit from another configuration.

WIP: Configuration inheritance is not implemented yet!

use Pest\Mutate\Mutators;

mutate('arithmetic only')
    ->extends('default')
    ->mutators(Mutators::SET_ARITHMETIC);

Options

The following options are available.

path()

CLI: --path

Limit the directories or files to mutate by providing one or more paths to a directory or file to test.

If no paths are provided, it defaults to the source directories configured in your phpunit.xml file.

mutate()
    ->path('src');

ignore()

CLI: --ignore

Ignore one or more directory or file paths.

mutate()
    ->ignore('src/Contracts');

class()

CLI: --class

Limit the mutations to one or more classes by providing one or more class names.

mutate()
    ->class(MyClass::class, OtherClass::class);

mutator()

CLI: --mutator

Choose the mutators you want to use. Choose from various sets or provide individual mutators. If not set, Mutators::SET_DEFAULT is used.

A list of all available mutators can be found in the Mutator Reference.

use Pest\Mutate\Mutators;

mutate()
    ->mutator(Mutators::SET_ARITHMETIC);
// or
mutate()
    ->mutator(Mutators::ARITHMETIC_PLUS_TO_MINUS, Mutators::ARITHMETIC_MINUS_TO_PLUS);

On the CLI you can provide a comma separated list of mutator names.

vendor/bin/pest --mutate --mutator=ArithmeticPlusToMinus,ArithmeticMinusToPlus

except()

CLI: --except

Exclude specific mutators from being used. Especially useful if you want to use a set of mutators but want to exclude some of them.

use Pest\Mutate\Mutators;

mutate()
    ->mutators(Mutators::SET_ARITHMETIC);
    ->except(Mutators::ARITHMETIC_PLUS_TO_MINUS);

coveredOnly()

CLI: --covered-only

Limit mutations to code that is covered by tests. This is especially helpful if you are running only a subset of your test suite. See Only run parts of your test suite.

mutate()
    ->coveredOnly();

uncommittedOnly()

CLI: --uncommitted-only

Limit mutations to code that has uncommitted changes.

mutate()
    ->uncommittedOnly();

changedOnly()

CLI: --changed-only

Limit mutations to code that has changed relative to a common ancestor of the given branch (defaults to main).

mutate()
    ->changedOnly(); // or ->changedOnly('add-xyz');

stopOnEscaped()

CLI: --stop-on-escaped

Stop execution upon first escaped mutant.

mutate()
    ->stopOnEscaped();

stopOnNotCovered()

CLI: --stop-on-not-covered

Stop execution upon first not covered mutant.

mutate()
    ->stopOnNotCovered();

bail()

CLI: --bail

Stop execution upon first not covered or escaped mutant.

mutate()
    ->bail();

retry()

CLI: --retry

If a mutation previously escaped, you typically want to run them first. In such cases, you can use the --retry option.

The --retry flag reorders your mutations by prioritizing the previously escaped mutations. If there were no past escaped mutations, the suite runs as usual.

Additionally, it will stop execution upon first escaped mutant.

mutate()
    ->retry();

min()

CLI: --min

Enforce a minimum mutation score threshold. For more information see Minimum Score Threshold Enforcement.

mutate()
    ->min(100);

You can pass an optional second parameter to ignore the minimum score threshold if zero mutations are generated. In this case Pest will exit with code 0.

mutate()
    ->min(100, failOnZeroMutations: false);

ignoreMinScoreOnZeroMutations()

CLI: --ignore-min-score-on-zero-mutations

Ignores the minimum score threshold if zero mutations are generated. In this case Pest will exit with code 0.

mutate()
    ->ignoreMinScoreOnZeroMutations();

--id

Run only the mutation with the given ID. You can find the ID of a mutation in the console output of a previous run.

vendor/bin/pest --mutate --id=fa6913f68aa87747

--no-cache

Disables the cache (This option is only available on the cli).

vendor/bin/pest --mutate --no-cache

--clear-cache

Clears the cache (This option is only available on the cli).

vendor/bin/pest --mutate --clear-cache

Performance

Mutation testing is potentially very time-consuming and resource intensive because of the sheer amount of possible mutations and tests to run them against.

Therefore, Pest Mutation Testing is optimized to limit the amount of mutations and tests to run against as much as possible. To achieve this, it uses the following strategies:

  • Limit the amount of possible mutations by having a carefully chosen set of mutators
  • Run only tests that covers the mutated code
  • It tries to reuse cached mutations
  • Run mutations in a reasonable order
  • Provide options to stop on first escaped or not covered mutation

But there is much more you can do to improve performance. Especially if you have a larger code base and/or you are using mutations testing while developing locally.

Use a code coverage driver

If you have a code coverage driver available, Pest will use it to only run tests that cover the mutated code.

Supports XDebug 3.0+ or PCOV.

Reduce the number of files to mutate

Reduce the number of mutations by only mutating a subset of your code base.

vendor/bin/pest --mutate --path=src/path/file.php

Reduce the number of classes to mutate

Reduce the number of mutations by only mutating a subset of your classes.

vendor/bin/pest --mutate --class="App\\MyClass,App\\OtherClass"

Only run parts of your test suite

Reduce the number of mutations and tests to execute by only running a subset of your test suite.

vendor/bin/pest --mutate --filter=SumTest

For more filter options see Filtering.

Only create mutations for covered files / lines

Reduce the number of mutations by only mutating code that is covered by tests. This is especially helpful if you are running only a subset of your test suite. See Only run parts of your test suite.

vendor/bin/pest --mutate --covered-only

Attention: Code not covered by tests will not be mutated. Ensure your test suite covers all code you want to mutate.

Run tests in parallel

Run tests against multiple mutations in parallel. This can significantly reduce the time it takes to run mutation tests.

Against a single mutation the tests are not run in parallel, regardless of the parallel option.

vendor/bin/pest --mutate --parallel

Reduce the number of mutators

Reduce the number of mutations by choosing a smaller set of mutators.

vendor/bin/pest --mutate --mutator=ArithmeticPlusToMinus

Profiling

You can profile the performance of the mutations by using the --profile option. It outputs a list of the ten slowest mutations.

vendor/bin/pest --mutate --profile

Ignoring Mutations

Ignore for a single line

Sometimes, you may want to prevent a line from being mutated. To do so, you may use the @pest-mutate-ignore annotation:

if($age >= 18) // @pest-mutate-ignore
    // ...
];

If you want to ignore only a specific mutator, you can add a comma separated list of mutator names:

if($age >= 18) // @pest-mutate-ignore: GreaterOrEqualToGreater
    // ...
];

Ignore for multiple lines

To ignore mutations on large parts of the code you can add the annotation to a class, method or statement to ignore all mutations within the elements scope.

To ignore only specific mutators, you can add a comma separated list of mutator names: @pest-mutate-ignore: GreaterOrEqualToGreater,IfNegated

Class level

/**
 * @pest-mutate-ignore
 */
class Test {
    // ...
}

Method or function level

/**
 * @pest-mutate-ignore
 */
public function test() {
    // ...
}

Statement level

/** @pest-mutate-ignore */
for($i = 0; $i < 10; $i++) {
    // ...
}

Minimum Score Threshold Enforcement

Just like code coverage, mutation coverage can also be enforced. You can use the --mutate and --min options to define the minimum threshold value for the mutation score. If the specified threshold is not met, Pest will report a failure.

./vendor/bin/pest --mutate --min=100

If zero mutations are generated, the score is considered to be 0 and Pest will report a failure. You can use the --ignore-min-score-on-zero-mutations option to ignore the minimum score threshold if zero mutations are generated. In this case Pest will exit with code 0.

./vendor/bin/pest --mutate --min=100 --ignore-min-score-on-zero-mutations

Custom Mutators

WIP: Custom mutators are not implemented yet!

You may want to create your own custom mutators. You can do so by creating a class that implements the Mutator interface.
This example will remove use statements.

namespace App\Mutators;

use Pest\Mutate\Contracts\Mutator;
use PhpParser\Node;
use PhpParser\NodeTraverser;

class RemoveUseStatement implements Mutator
{
    public static function can(Node $node): bool
    {
         return $node instanceof Node\Stmt\Use_;
    }

    public static function mutate(Node $node): int
    {
        return NodeTraverser::REMOVE_NODE;
    }
}

Afterward you can use your mutator.

use App\Mutators\RemoveUseStatement;

mutate()
    ->mutators(RemoveUseStatement::class);

In the CLI you must provide the full class name.

vendor/bin/pest --mutate --mutators="App\\Mutators\\RemoveUseStatement"

Mutator Reference

A comprehensive list of all mutators available.

Sets

The mutators are grouped in sets.

Default

A set of mutators that are enabled by default. Which keeps a good balance between performance and mutation coverage.

This set consists of various mutators from different sets. Mutators included are indicated with a asterisk (*).

ArithmeticSet

ArraySet

AssignmentSet

CastingSet

ControlStructuresSet

EqualitySet

LogicalSet

MathSet

NumberSet

RemovalSet

ReturnSet

StringSet

VisibilitySet

LaravelSet

Mutators

An alphabetical list of all mutators.

AlwaysReturnEmptyArray (*)

Set: Return

Mutates a return statement to an empty array

return [1];  // [tl! remove]
return [];  // [tl! add]

AlwaysReturnNull (*)

Set: Return

Mutates a return statement to null if it is not null

return $a;  // [tl! remove]
return null;  // [tl! add]

ArrayKeyFirstToArrayKeyLast (*)

Set: Array

Replaces array_key_first with array_key_last.

$a = array_key_first([1, 2, 3]);  // [tl! remove]
$a = array_key_last([1, 2, 3]);  // [tl! add]

ArrayKeyLastToArrayKeyFirst (*)

Set: Array

Replaces array_key_last with array_key_first.

$a = array_key_last([1, 2, 3]);  // [tl! remove]
$a = array_key_first([1, 2, 3]);  // [tl! add]

ArrayPopToArrayShift (*)

Set: Array

Replaces array_pop with array_shift.

$a = array_pop([1, 2, 3]);  // [tl! remove]
$a = array_shift([1, 2, 3]);  // [tl! add]

ArrayShiftToArrayPop (*)

Set: Array

Replaces array_shift with array_pop.

$a = array_shift([1, 2, 3]);  // [tl! remove]
$a = array_pop([1, 2, 3]);  // [tl! add]

BitwiseAndToBitwiseOr (*)

Set: Arithmetic

Replaces & with |.

$c = $a & $b;  // [tl! remove]
$c = $a | $b;  // [tl! add]

BitwiseAndToBitwiseOr (*)

Set: Assignment

Replaces &= with |=.

$a &= $b;  // [tl! remove]
$a |= $b;  // [tl! add]

BitwiseOrToBitwiseAnd (*)

Set: Arithmetic

Replaces | with &.

$c = $a | $b;  // [tl! remove]
$c = $a & $b;  // [tl! add]

BitwiseOrToBitwiseAnd (*)

Set: Assignment

Replaces |= with &=.

$a |= $b;  // [tl! remove]
$a &= $b;  // [tl! add]

BitwiseXorToBitwiseAnd (*)

Set: Arithmetic

Replaces ^ with &.

$c = $a ^ $b;  // [tl! remove]
$c = $a & $b;  // [tl! add]

BitwiseXorToBitwiseAnd (*)

Set: Assignment

Replaces ^= with &=.

$a ^= $b;  // [tl! remove]
$a &= $b;  // [tl! add]

BooleanAndToBooleanOr (*)

Set: Logical

Converts the boolean and operator to the boolean or operator.

if ($a && $b) {  // [tl! remove]
if ($a || $b) {  // [tl! add]
    // ...
}

BooleanOrToBooleanAnd (*)

Set: Logical

Converts the boolean or operator to the boolean and operator.

if ($a || $b) {  // [tl! remove]
if ($a && $b) {  // [tl! add]
    // ...
}

BreakToContinue (*)

Set: ControlStructures

Replaces break with continue.

foreach ($items as $item) {
    if ($item === 'foo') {
        break;  // [tl! remove]
        continue;  // [tl! add]
    }
}

CeilToFloor (*)

Set: Math

Replaces ceil function with floor function.

$a = ceil(1.2);  // [tl! remove]
$a = floor(1.2);  // [tl! add]

CeilToRound (*)

Set: Math

Replaces ceil function with round function.

$a = ceil(1.2);  // [tl! remove]
$a = round(1.2);  // [tl! add]

CoalesceEqualToEqual (*)

Set: Assignment

Replaces ??= with =.

$a ??= $b;  // [tl! remove]
$a = $b;  // [tl! add]

CoalesceRemoveLeft (*)

Set: Logical

Removes the left side of the coalesce operator.

return $a ?? $b;  // [tl! remove]
return $b;  // [tl! add]

ConcatEqualToEqual (*)

Set: Assignment

Replaces .= with =.

$a .= $b;  // [tl! remove]
$a = $b;  // [tl! add]

ConcatRemoveLeft (*)

Set: String

Removes the left part of a concat expression.

$a = 'Hello' . ' World';  // [tl! remove]
$a = ' World';  // [tl! add]

ConcatRemoveRight (*)

Set: String

Removes the right part of a concat expression.

$a = 'Hello' . ' World';  // [tl! remove]
$a = 'Hello';  // [tl! add]

ConcatSwitchSides (*)

Set: String

Switches the sides of a concat expression.

$a = 'Hello' . ' World';  // [tl! remove]
$a = ' World' . 'Hello';  // [tl! add]

ConstantProtectedToPrivate

Set: Visibility

Mutates a protected constant to a private constant

protected const FOO = true;  // [tl! remove]
private const FOO = true;  // [tl! add]

ConstantPublicToProtected

Set: Visibility

Mutates a public constant to a protected constant

public const FOO = true;  // [tl! remove]
protected const FOO = true;  // [tl! add]

ContinueToBreak (*)

Set: ControlStructures

Replaces continue with break.

foreach ($items as $item) {
    if ($item === 'foo') {
        continue;  // [tl! remove]
        break;  // [tl! add]
    }
}

DecrementFloat (*)

Set: Number

Decrements a float number by 1.

$a = 1.2;  // [tl! remove]
$a = 0.2;  // [tl! add]

DecrementInteger (*)

Set: Number

Decrements an integer number by 1.

$a = 1;  // [tl! remove]
$a = 0;  // [tl! add]

DivideEqualToMultiplyEqual (*)

Set: Assignment

Replaces /= with *=.

$a /= $b;  // [tl! remove]
$a *= $b;  // [tl! add]

DivisionToMultiplication (*)

Set: Arithmetic

Replaces / with *.

$c = $a / $b;  // [tl! remove]
$c = $a * $b;  // [tl! add]

DoWhileAlwaysFalse (*)

Set: ControlStructures

Makes the condition in a do-while loop always false.

do {
    // ...
} while ($a < 100);  // [tl! remove]
} while (false);  // [tl! add]

ElseIfNegated (*)

Set: ControlStructures

Negates the condition in an elseif statement.

if ($a === 1) {
    // ...
} elseif ($a === 2) {  // [tl! remove]
} elseif (!($a === 2)) {  // [tl! add]
    // ...
}

EmptyStringToNotEmpty (*)

Set: String

Changes an empty string to a non-empty string.

$a = '';  // [tl! remove]
$a = 'PEST Mutator was here!';  // [tl! add]

EqualToIdentical (*)

Set: Equality

Converts the equality operator to the identical operator.

if ($a == $b) {  // [tl! remove]
if ($a === $b) {  // [tl! add]
    // ...
}

EqualToNotEqual (*)

Set: Equality

Converts the equality operator to the not equal operator.

if ($a == $b) {  // [tl! remove]
if ($a != $b) {  // [tl! add]
    // ...
}

FalseToTrue (*)

Set: Logical

Converts false to true.

if (false) {  // [tl! remove]
if (true) {  // [tl! add]
    // ...
}

FloorToCiel (*)

Set: Math

Replaces floor function with ceil function.

$a = floor(1.2);  // [tl! remove]
$a = ceil(1.2);  // [tl! add]

FloorToRound (*)

Set: Math

Replaces floor function with round function.

$a = floor(1.2);  // [tl! remove]
$a = round(1.2);  // [tl! add]

ForAlwaysFalse (*)

Set: ControlStructures

Makes the condition in a for loop always false.

for ($i = 0; $i < 10; $i++) {  // [tl! remove]
for ($i = 0; false; $i++) {  // [tl! add]
    // ...
}

ForeachEmptyIterable (*)

Set: ControlStructures

Replaces the iterable in a foreach loop with an empty array.

foreach ($items as $item) {  // [tl! remove]
foreach ([] as $item) {  // [tl! add]
    // ...
}

FunctionProtectedToPrivate

Set: Visibility

Mutates a protected function to a private function

protected function foo(): bool  // [tl! remove]
private function foo(): bool  // [tl! add]
{
    return true;
}

FunctionPublicToProtected (*)

Set: Visibility

Mutates a public function to a protected function

public function foo(): bool  // [tl! remove]
protected function foo(): bool  // [tl! add]
{
    return true;
}

GreaterOrEqualToGreater (*)

Set: Equality

Converts the greater or equal operator to the greater operator.

if ($a >= $b) {  // [tl! remove]
if ($a > $b) {  // [tl! add]
    // ...
}

GreaterOrEqualToSmaller (*)

Set: Equality

Converts the greater or equal operator to the smaller operator.

if ($a >= $b) {  // [tl! remove]
if ($a < $b) {  // [tl! add]
    // ...
}

GreaterToGreaterOrEqual (*)

Set: Equality

Converts the greater operator to the greater or equal operator.

if ($a > $b) {  // [tl! remove]
if ($a >= $b) {  // [tl! add]
    // ...
}

GreaterToSmallerOrEqual (*)

Set: Equality

Converts the greater operator to the smaller or equal operator.

if ($a > $b) {  // [tl! remove]
if ($a <= $b) {  // [tl! add]
    // ...
}

IdenticalToEqual

Set: Equality

Converts the identical operator to the equality operator.

if ($a === $b) {  // [tl! remove]
if ($a == $b) {  // [tl! add]
    // ...
}

IdenticalToNotIdentical (*)

Set: Equality

Converts the identical operator to the not identical operator.

if ($a === $b) {  // [tl! remove]
if ($a !== $b) {  // [tl! add]
    // ...
}

IfNegated (*)

Set: ControlStructures

Negates the condition in an if statement.

if ($a === 1) {  // [tl! remove]
if (!($a === 1)) {  // [tl! add]
    // ...
}

IncrementFloat (*)

Set: Number

Increments a float number by 1.

$a = 1.2;  // [tl! remove]
$a = 2.2;  // [tl! add]

IncrementInteger (*)

Set: Number

Increments an integer number by 1.

$a = 1;  // [tl! remove]
$a = 2;  // [tl! add]

InstanceOfToFalse (*)

Set: Logical

Converts instanceof to false.

if ($a instanceof $b) {  // [tl! remove]
if (false) {  // [tl! add]
    // ...
}

InstanceOfToTrue (*)

Set: Logical

Converts instanceof to true.

if ($a instanceof $b) {  // [tl! remove]
if (true) {  // [tl! add]
    // ...
}

LaravelRemoveStringableUpper

Set: Laravel

Removes the upper method call from a stringable object.

Str::of('hello')->upper();  // [tl! remove]
Str::of('hello');  // [tl! add]

LaravelUnwrapStrUpper

Set: Laravel

Unwraps the string upper method call.

$a = Illuminate\Support\Str::upper('foo');  // [tl! remove]
$a = 'foo';  // [tl! add]

LogicalAndToLogicalOr (*)

Set: Logical

Converts the logical and operator to the logical or operator.

if ($a && $b) {  // [tl! remove]
if ($a || $b) {  // [tl! add]
    // ...
}

LogicalOrToLogicalAnd (*)

Set: Logical

Converts the logical or operator to the logical and operator.

if ($a || $b) {  // [tl! remove]
if ($a && $b) {  // [tl! add]
    // ...
}

LogicalXorToLogicalAnd (*)

Set: Logical

Converts the logical xor operator to the logical and operator.

if ($a xor $b) {  // [tl! remove]
if ($a && $b) {  // [tl! add]
    // ...
}

MaxToMin (*)

Set: Math

Replaces max function with min function.

$a = max(1, 2);  // [tl! remove]
$a = min(1, 2);  // [tl! add]

MinToMax (*)

Set: Math

Replaces min function with max function.

$a = min(1, 2);  // [tl! remove]
$a = max(1, 2);  // [tl! add]

MinusEqualToPlusEqual (*)

Set: Assignment

Replaces -= with +=.

$a -= $b;  // [tl! remove]
$a += $b;  // [tl! add]

MinusToPlus (*)

Set: Arithmetic

Replaces - with +.

$c = $a - $b;  // [tl! remove]
$c = $a + $b;  // [tl! add]

ModulusEqualToMultiplyEqual (*)

Set: Assignment

Replaces %= with *=.

$a %= $b;  // [tl! remove]
$a *= $b;  // [tl! add]

ModulusToMultiplication (*)

Set: Arithmetic

Replaces % with *.

$c = $a % $b;  // [tl! remove]
$c = $a * $b;  // [tl! add]

MultiplicationToDivision (*)

Set: Arithmetic

Replaces * with /.

$c = $a * $b;  // [tl! remove]
$c = $a / $b;  // [tl! add]

MultiplyEqualToDivideEqual (*)

Set: Assignment

Replaces *= with /=.

$a *= $b;  // [tl! remove]
$a /= $b;  // [tl! add]

NotEmptyStringToEmpty (*)

Set: String

Changes a non-empty string to an empty string.

$a = 'Hello World';  // [tl! remove]
$a = '';  // [tl! add]

NotEqualToEqual (*)

Set: Equality

Converts the not equal operator to the equal operator.

if ($a != $b) {  // [tl! remove]
if ($a == $b) {  // [tl! add]
    // ...
}

NotEqualToNotIdentical (*)

Set: Equality

Converts the not equal operator to the not identical operator.

if ($a != $b) {  // [tl! remove]
if ($a !== $b) {  // [tl! add]
    // ...
}

NotIdenticalToIdentical (*)

Set: Equality

Converts the not identical operator to the identical operator.

if ($a !== $b) {  // [tl! remove]
if ($a === $b) {  // [tl! add]
    // ...
}

NotIdenticalToNotEqual

Set: Equality

Converts the not identical operator to the not equal operator.

if ($a !== $b) {  // [tl! remove]
if ($a != $b) {  // [tl! add]
    // ...
}

PlusEqualToMinusEqual (*)

Set: Assignment

Replaces += with -=.

$a += $b;  // [tl! remove]
$a -= $b;  // [tl! add]

PlusToMinus (*)

Set: Arithmetic

Replaces + with -.

$c = $a + $b;  // [tl! remove]
$c = $a - $b;  // [tl! add]

PostDecrementToPostIncrement (*)

Set: Arithmetic

Replaces -- with ++.

$b = $a--;  // [tl! remove]
$b = $a++;  // [tl! add]

PostIncrementToPostDecrement (*)

Set: Arithmetic

Replaces ++ with --.

$b = $a++;  // [tl! remove]
$b = $a--;  // [tl! add]

PowerEqualToMultiplyEqual (*)

Set: Assignment

Replaces **= with *=.

$a **= $b;  // [tl! remove]
$a *= $b;  // [tl! add]

PowerToMultiplication (*)

Set: Arithmetic

Replaces ** with *.

$c = $a ** $b;  // [tl! remove]
$c = $a * $b;  // [tl! add]

PreDecrementToPreIncrement (*)

Set: Arithmetic

Replaces -- with ++.

$b = --$a;  // [tl! remove]
$b = ++$a;  // [tl! add]

PreIncrementToPreDecrement (*)

Set: Arithmetic

Replaces ++ with --.

$b = ++$a;  // [tl! remove]
$b = --$a;  // [tl! add]

PropertyProtectedToPrivate

Set: Visibility

Mutates a protected property to a private property

protected bool $foo = true;  // [tl! remove]
private bool $foo = true;  // [tl! add]

PropertyPublicToProtected

Set: Visibility

Mutates a public property to a protected property

public bool $foo = true;  // [tl! remove]
protected bool $foo = true;  // [tl! add]

RemoveArrayCast (*)

Set: Casting

Removes array cast.

$a = (array) $b;  // [tl! remove]
$a = $b;          // [tl! add]

RemoveArrayItem (*)

Set: Removal

Removes an item from an array

return [
    'foo' => 1,  // [tl! remove]
    'bar' => 2,
];

RemoveBooleanCast (*)

Set: Casting

Removes boolean cast.

$a = (bool) $b;  // [tl! remove]
$a = $b;         // [tl! add]

RemoveDoubleCast (*)

Set: Casting

Removes double cast.

$a = (double) $b;  // [tl! remove]
$a = $b;           // [tl! add]

RemoveEarlyReturn (*)

Set: Removal

Removes an early return statement

if ($a > $b) {
    return true // [tl! remove]
}

return false;

RemoveFunctionCall (*)

Set: Removal

Removes a function call

foo();  // [tl! remove]

RemoveIntegerCast (*)

Set: Casting

Removes integer cast.

$a = (int) $b;  // [tl! remove]
$a = $b;        // [tl! add]

RemoveMethodCall (*)

Set: Removal

Removes a method call

$this->foo();  // [tl! remove]

RemoveNot (*)

Set: Logical

Removes the not operator.

if (!$a) {  // [tl! remove]
if ($a) {  // [tl! add]
    // ...
}

RemoveNullSafeOperator (*)

Set: Removal

Converts nullsafe method and property calls to regular calls.

$a?->b();  // [tl! remove]
$a->b();  // [tl! add]

RemoveObjectCast (*)

Set: Casting

Removes object cast.

$a = (object) $b;  // [tl! remove]
$a = $b;           // [tl! add]

RemoveStringCast (*)

Set: Casting

Removes string cast.

$a = (string) $b;  // [tl! remove]
$a = $b;           // [tl! add]

RoundToCeil (*)

Set: Math

Replaces round function with ceil function.

$a = round(1.2);  // [tl! remove]
$a = ceil(1.2);  // [tl! add]

RoundToFloor (*)

Set: Math

Replaces round function with floor function.

$a = round(1.2);  // [tl! remove]
$a = floor(1.2);  // [tl! add]

ShiftLeftToShiftRight (*)

Set: Arithmetic

Replaces << with >>.

$b = $a << 1;  // [tl! remove]
$b = $a >> 1;  // [tl! add]

ShiftLeftToShiftRight (*)

Set: Assignment

Replaces <<= with >>=.

$a <<= $b;  // [tl! remove]
$a >>= $b;  // [tl! add]

ShiftRightToShiftLeft (*)

Set: Arithmetic

Replaces >> with <<.

$b = $a >> 1;  // [tl! remove]
$b = $a << 1;  // [tl! add]

ShiftRightToShiftLeft (*)

Set: Assignment

Replaces >>= with <<=.

$a >>= $b;  // [tl! remove]
$a <<= $b;  // [tl! add]

SmallerOrEqualToGreater (*)

Set: Equality

Converts the smaller or equal operator to the greater operator.

if ($a <= $b) {  // [tl! remove]
if ($a > $b) {  // [tl! add]
    // ...
}

SmallerOrEqualToSmaller (*)

Set: Equality

Converts the smaller or equal operator to the smaller operator.

if ($a <= $b) {  // [tl! remove]
if ($a < $b) {  // [tl! add]
    // ...
}

SmallerToGreaterOrEqual (*)

Set: Equality

Converts the smaller operator to the greater or equal operator.

if ($a < $b) {  // [tl! remove]
if ($a >= $b) {  // [tl! add]
    // ...
}

SmallerToSmallerOrEqual (*)

Set: Equality

Converts the smaller operator to the smaller or equal operator.

if ($a < $b) {  // [tl! remove]
if ($a <= $b) {  // [tl! add]
    // ...
}

SpaceshipSwitchSides (*)

Set: Equality

Switches the sides of the spaceship operator.

return $a <=> $b;  // [tl! remove]
return $b <=> $a;  // [tl! add]

StrEndsWithToStrStartsWith (*)

Set: String

Replaces str_ends_with with str_starts_with.

$a = str_ends_with('Hello World', 'World');  // [tl! remove]
$a = str_starts_with('Hello World', 'World');  // [tl! add]

StrStartsWithToStrEndsWith (*)

Set: String

Replaces str_starts_with with str_ends_with.

$a = str_starts_with('Hello World', 'World');  // [tl! remove]
$a = str_ends_with('Hello World', 'World');  // [tl! add]

TernaryNegated (*)

Set: ControlStructures

Negates the condition in a ternary statement.

$a = $b ? 1 : 2;  // [tl! remove]
$a = !$b ? 1 : 2;  // [tl! add]

TrueToFalse (*)

Set: Logical

Converts true to false.

if (true) {  // [tl! remove]
if (false) {  // [tl! add]
    // ...
}

UnwrapArrayChangeKeyCase (*)

Set: Array

Unwraps array_change_key_case calls.

$a = array_change_key_case(['foo' => 'bar'], CASE_UPPER);  // [tl! remove]
$a = ['foo' => 'bar'];  // [tl! add]

UnwrapArrayChunk (*)

Set: Array

Unwraps array_chunk calls.

$a = array_chunk([1, 2, 3], 2);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayColumn (*)

Set: Array

Unwraps array_column calls.

$a = array_column([['id' => 1], ['id' => 2]], 'id');  // [tl! remove]
$a = [['id' => 1], ['id' => 2]];  // [tl! add]

UnwrapArrayCombine (*)

Set: Array

Unwraps array_combine calls.

$a = array_combine([1, 2, 3], [3, 4]);  // [tl! remove]
$a = [1, 2, 3]  // [tl! add]

UnwrapArrayCountValues (*)

Set: Array

Unwraps array_count_values calls.

$a = array_count_values([1, 2, 3]);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayDiff (*)

Set: Array

Unwraps array_diff calls.

$a = array_diff([1, 2, 3], [1, 2]);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayDiffAssoc (*)

Set: Array

Unwraps array_diff_assoc calls.

$a = array_diff_assoc(['foo' => 1, 'bar' => 2], ['foo' => 1])  // [tl! remove]
$a = ['foo' => 1, 'bar' => 2];  // [tl! add]

UnwrapArrayDiffKey (*)

Set: Array

Unwraps array_diff_key calls.

$a = array_diff_key(['foo' => 1, 'bar' => 2], ['foo' => 1]);  // [tl! remove]
$a = ['foo' => 1, 'bar' => 2];  // [tl! add]

UnwrapArrayDiffUassoc (*)

Set: Array

Unwraps array_diff_uassoc calls.

$a = array_diff_uassoc([1, 2, 3], [1, 2], 'strcmp');  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayDiffUkey (*)

Set: Array

Unwraps array_diff_ukey calls.

$a = array_diff_ukey(['foo' => 1, 'bar' => 2], ['foo' => 1], 'strcmp');  // [tl! remove]
$a = ['foo' => 1, 'bar' => 2];  // [tl! add]

UnwrapArrayFilter (*)

Set: Array

Unwraps array_filter calls.

$a = array_filter([1, 2, 3], fn($value) => $value > 2);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayFlip (*)

Set: Array

Unwraps array_flip calls.

$a = array_flip(['foo' => 1, 'bar' => 2]);  // [tl! remove]
$a = ['foo' => 1, 'bar' => 2];  // [tl! add]

UnwrapArrayIntersect (*)

Set: Array

Unwraps array_intersect calls.

$a = array_intersect([1, 2, 3], [1, 2]);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayIntersectAssoc (*)

Set: Array

Unwraps array_intersect_assoc calls.

$a = array_intersect_assoc(['foo' => 1, 'bar' => 2], ['foo' => 1]);  // [tl! remove]
$a = ['foo' => 1, 'bar' => 2];  // [tl! add]

UnwrapArrayIntersectKey (*)

Set: Array

Unwraps array_intersect_key calls.

$a = array_intersect_key(['foo' => 1, 'bar' => 2], ['foo' => 1]);  // [tl! remove]
$a = ['foo' => 1, 'bar' => 2];  // [tl! add]

UnwrapArrayIntersectUassoc (*)

Set: Array

Unwraps array_intersect_uassoc calls.

$a = array_intersect_uassoc([1, 2, 3], [1, 2], 'strcmp');  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayIntersectUkey (*)

Set: Array

Unwraps array_intersect_ukey calls.

$a = array_intersect_ukey(['foo' => 1, 'bar' => 2], ['foo' => 1], 'strcmp');  // [tl! remove]
$a = ['foo' => 1, 'bar' => 2];  // [tl! add]

UnwrapArrayKeys (*)

Set: Array

Unwraps array_keys calls.

$a = array_keys(['foo' => 1, 'bar' => 2]);  // [tl! remove]
$a = ['foo' => 1, 'bar' => 2];  // [tl! add]

UnwrapArrayMap (*)

Set: Array

Unwraps array_map calls.

$a = array_map(fn ($value) => $value + 1, [1, 2, 3]);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayMerge (*)

Set: Array

Unwraps array_merge calls.

$a = array_merge([1, 2, 3], [4, 5, 6]);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayMergeRecursive (*)

Set: Array

Unwraps array_merge_recursive calls.

$a = array_merge_recursive([1, 2, 3], [4, 5, 6]);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayPad (*)

Set: Array

Unwraps array_pad calls.

$a = array_pad([1, 2, 3], 5, 0);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayReduce (*)

Set: Array

Unwraps array_reduce calls.

$a = array_reduce([1, 2, 3], fn ($carry, $item) => $carry + $item, 0);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayReplace (*)

Set: Array

Unwraps array_replace calls.

$a = array_replace([1, 2, 3], ['a', 'b', 'c']);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayReplaceRecursive (*)

Set: Array

Unwraps array_replace_recursive calls.

$a = array_replace_recursive([1, 2, 3], ['a', 'b', 'c']);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayReverse (*)

Set: Array

Unwraps array_reverse calls.

$a = array_reverse([1, 2, 3]);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArraySlice (*)

Set: Array

Unwraps array_slice calls.

$a = array_slice([1, 2, 3], 1, 2);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArraySplice (*)

Set: Array

Unwraps array_splice calls.

$a = array_splice([1, 2, 3], 0, 2, ['a', 'b']);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayUdiff (*)

Set: Array

Unwraps array_udiff calls.

$a = array_udiff([1, 2, 3], [1, 2, 4], fn($a, $b) => $a <=> $b);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayUdiffAssoc (*)

Set: Array

Unwraps array_udiff_assoc calls.

$a = array_udiff_assoc([1, 2, 3], [1, 2, 4], fn ($a, $b) => $a <=> $b);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayUdiffUassoc (*)

Set: Array

Unwraps array_udiff_uassoc calls.

$a = array_udiff_uassoc([1, 2, 3], [1, 2, 4], fn ($a, $b) => $a <=> $b, fn ($a, $b) => $a <=> $b);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayUintersect (*)

Set: Array

Unwraps array_uintersect calls.

$a = array_uintersect([1, 2, 3], [1, 2, 4], fn ($a, $b) => $a <=> $b);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayUintersectAssoc (*)

Set: Array

Unwraps array_uintersect_assoc calls.

$a = array_uintersect_assoc([1, 2, 3], [1, 2, 4], fn ($a, $b) => $a <=> $b);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayUintersectUassoc (*)

Set: Array

Unwraps array_uintersect_uassoc calls.

$a = array_uintersect_uassoc([1, 2, 3], [1, 2, 4], fn ($a, $b) => $a <=> $b, fn ($a, $b) => $a <=> $b);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayUnique (*)

Set: Array

Unwraps array_unique calls.

$a = array_unique([1, 2, 3]);  // [tl! remove]
$a = [1, 2, 3];  // [tl! add]

UnwrapArrayValues (*)

Set: Array

Unwraps array_values calls.

$a = array_values(['a' => 1, 'b' => 2, 'c' => 3]);  // [tl! remove]
$a = ['a' => 1, 'b' => 2, 'c' => 3];  // [tl! add]

UnwrapChop (*)

Set: String

Unwraps chop calls.

$a = chop('Hello World ', ' ');  // [tl! remove]
$a = 'Hello World ';  // [tl! add]

UnwrapChunkSplit (*)

Set: String

Unwraps chunk_split calls.

$a = chunk_split('Hello World', 1, ' ');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapHtmlEntityDecode (*)

Set: String

Unwraps html_entity_decode calls.

$a = html_entity_decode('&lt;h1&gt;Hello World&lt;/h1&gt;');  // [tl! remove]
$a = '&lt;h1&gt;Hello World&lt;/h1&gt;';  // [tl! add]

UnwrapHtmlentities (*)

Set: String

Unwraps htmlentities calls.

$a = htmlentities('<h1>Hello World</h1>');  // [tl! remove]
$a = '<h1>Hello World</h1>';  // [tl! add]

UnwrapHtmlspecialchars (*)

Set: String

Unwraps htmlspecialchars calls.

$a = htmlspecialchars('<h1>Hello World</h1>');  // [tl! remove]
$a = '<h1>Hello World</h1>';  // [tl! add]

UnwrapHtmlspecialcharsDecode (*)

Set: String

Unwraps htmlspecialchars_decode calls.

$a = htmlspecialchars_decode('&lt;h1&gt;Hello World&lt;/h1&gt;');  // [tl! remove]
$a = '&lt;h1&gt;Hello World&lt;/h1&gt;';  // [tl! add]

UnwrapLcfirst (*)

Set: String

Unwraps lcfirst calls.

$a = lcfirst('Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapLtrim (*)

Set: String

Unwraps ltrim calls.

$a = ltrim(' Hello World');  // [tl! remove]
$a = ' Hello World';  // [tl! add]

UnwrapMd5 (*)

Set: String

Unwraps md5 calls.

$a = md5('Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapNl2br (*)

Set: String

Unwraps nl2br calls.

$a = nl2br('Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapRtrim (*)

Set: String

Unwraps rtrim calls.

$a = rtrim('Hello World ');  // [tl! remove]
$a = 'Hello World ';  // [tl! add]

UnwrapStrIreplace (*)

Set: String

Unwraps str_ireplace calls.

$a = str_ireplace('Hello', 'Hi', 'Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapStrPad (*)

Set: String

Unwraps str_pad calls.

$a = str_pad('Hello World', 20, '-');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapStrRepeat (*)

Set: String

Unwraps str_repeat calls.

$a = str_repeat('Hello World', 2);  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapStrReplace (*)

Set: String

Unwraps str_replace calls.

$a = str_replace('Hello', 'Hi', 'Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapStrShuffle (*)

Set: String

Unwraps str_shuffle calls.

$a = str_shuffle('Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapStripTags (*)

Set: String

Unwraps strip_tags calls.

$a = strip_tags('Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapStrrev (*)

Set: String

Unwraps strrev calls.

$a = strrev('Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapStrtolower (*)

Set: String

Unwraps strtolower calls.

$a = strtolower('Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapStrtoupper (*)

Set: String

Unwraps strtoupper calls.

$a = strtoupper('Hello World');  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapSubstr (*)

Set: String

Unwraps substr calls.

$a = substr('Hello World', 0, 5);  // [tl! remove]
$a = 'Hello World';  // [tl! add]

UnwrapTrim (*)

Set: String

Unwraps trim calls.

$a = trim(' Hello World ');  // [tl! remove]
$a = ' Hello World ';  // [tl! add]

UnwrapUcfirst (*)

Set: String

Unwraps ucfirst calls.

$a = ucfirst('hello world');  // [tl! remove]
$a = 'hello world';  // [tl! add]

UnwrapUcwords (*)

Set: String

Unwraps ucwords calls.

$a = ucwords('hello world');  // [tl! remove]
$a = 'hello world';  // [tl! add]

UnwrapWordwrap (*)

Set: String

Unwraps wordwrap calls.

$a = wordwrap('Hello World', 5);  // [tl! remove]
$a = 'Hello World';  // [tl! add]

WhileAlwaysFalse (*)

Set: ControlStructures

Makes the condition in a while loop always false.

while ($a < 100) {  // [tl! remove]
while (false) {  // [tl! add]
    // ...
}