diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 17328a08..6eb49c69 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -145,6 +145,54 @@ jobs: - name: "Tests" run: "make tests" + mutation-testing: + name: "Mutation Testing" + runs-on: "ubuntu-latest" + needs: ["tests", "static-analysis"] + + strategy: + fail-fast: false + matrix: + php-version: + - "8.2" + - "8.3" + - "8.4" + + steps: + - name: "Checkout" + uses: actions/checkout@v5 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "pcov" + php-version: "${{ matrix.php-version }}" + ini-file: development + extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb + tools: infection:0.31.4 + + - name: "Allow installing on PHP 8.4" + if: matrix.php-version == '8.4' + run: "composer config platform.php 8.3.99" + + - name: "Install dependencies" + run: "composer install --no-interaction --no-progress" + + - uses: "actions/download-artifact@v4" + with: + name: "result-cache-${{ matrix.php-version }}" + path: "tmp/" + + - name: "Run infection" + run: | + git fetch --depth=1 origin $GITHUB_BASE_REF + infection --git-diff-base=origin/$GITHUB_BASE_REF --git-diff-lines --ignore-msi-with-no-mutations --min-msi=100 --min-covered-msi=100 --log-verbosity=all --debug + + - uses: "actions/upload-artifact@v4" + with: + name: "infection-log-${{ matrix.php-version }}" + path: "tmp/infection.log" + static-analysis: name: "PHPStan" runs-on: "ubuntu-latest" @@ -189,3 +237,9 @@ jobs: - name: "PHPStan" run: "make phpstan" + + - uses: "actions/upload-artifact@v4" + with: + # "update-packages" is not relevant for the download-artifact counterpart, but we need it here to get unique artifact names across all jobs + name: "result-cache-${{ matrix.php-version }}${{ matrix.update-packages && '-packages-updated' || '' }}" + path: "tmp/resultCache.php" diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 00000000..7afd7230 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,17 @@ +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "timeout": 30, + "source": { + "directories": [ + "src" + ] + }, + "staticAnalysisTool": "phpstan", + "logs": { + "text": "tmp/infection.log" + }, + "mutators": { + "@default": false, + "PHPStan\\Infection\\TrinaryLogicMutator": true + } +} diff --git a/phpstan.neon b/phpstan.neon index eabc5225..385b9690 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -18,11 +18,14 @@ parameters: - src - tests + resultCachePath: tmp/resultCache.php + excludePaths: - tests/*/data/* - tests/*/data-attributes/* - tests/*/data-php-*/* - tests/Rules/Doctrine/ORM/entity-manager.php + - tests/Infection/ reportUnmatchedIgnoredErrors: false diff --git a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php index 69f9791a..fae07a6e 100644 --- a/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php +++ b/src/Rules/Doctrine/ORM/QueryBuilderDqlRule.php @@ -69,6 +69,15 @@ public function processNode(Node $node, Scope $scope): array return []; } + // testing stuff + $obj = (new ObjectType('Doctrine\ORM\QueryBuilder'))->isSuperTypeOf($calledOnType); + if ($obj->yes()) { + $x = 1; + } else { + $x = 2; + } + + try { $dqlType = $scope->getType(new MethodCall($node, new Node\Identifier('getDQL'), [])); } catch (Throwable $e) { diff --git a/tests/Infection/TrinaryLogicMutator.php b/tests/Infection/TrinaryLogicMutator.php new file mode 100644 index 00000000..67e9b530 --- /dev/null +++ b/tests/Infection/TrinaryLogicMutator.php @@ -0,0 +1,69 @@ + + */ +final class TrinaryLogicMutator implements Mutator +{ + + public static function getDefinition(): Definition + { + return new Definition( + <<<'TXT' + Replaces TrinaryLogic->yes() with !TrinaryLogic->no() and vice versa. + TXT + , + MutatorCategory::ORTHOGONAL_REPLACEMENT, + null, + <<<'DIFF' + - $type->isBoolean()->yes(); + + !$type->isBoolean()->no(); + DIFF, + ); + } + + public function getName(): string + { + return 'TrinaryLogicMutator'; + } + + public function canMutate(Node $node): bool + { + if (!$node instanceof Node\Expr\MethodCall) { + return false; + } + + if (!$node->name instanceof Node\Identifier) { + return false; + } + + if (!in_array($node->name->name, ['yes', 'no'], true)) { + return false; + } + + return true; + } + + public function mutate(Node $node): iterable + { + if (!$node->name instanceof Node\Identifier) { + throw new LogicException(); + } + + if ($node->name->name === 'yes') { + yield new Node\Expr\BooleanNot(new Node\Expr\MethodCall($node->var, 'no')); + } else { + yield new Node\Expr\BooleanNot(new Node\Expr\MethodCall($node->var, 'yes')); + } + } + +}