From 240d58f311a2bbec45df7ab56072daebe3b30454 Mon Sep 17 00:00:00 2001 From: "v.razuvaev" Date: Sun, 1 Nov 2026 21:33:04 +0300 Subject: [PATCH] Count uncovered code in mutation testing and cover the gap it exposed infection --with-uncovered now gates the build; PublicVisibility off and default-parameter signature lines ignored. Adds a unit test for the docblock splitter failing. --- CHANGELOG.md | 6 ++++++ composer.json | 5 ++++- infection.json5 | 20 +++++++++++++++++++- tests/DocblockTypeRewriterTest.php | 26 ++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c87ed2e..957aea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +- Mutation testing now counts code the tests execute without naming it in `#[Covers]` + (`infection --with-uncovered`), with `PublicVisibility` off and default-parameter + signature lines ignored; unit test added for the docblock splitter failing (`preg_split` returning false). No `src/` changes. + ## 1.0.1 — 2026-09-27 - Package `type` changed to `rector-extension` so the rule pack is discoverable on Packagist (`type=rector-extension`) and recognised by `rector/extension-installer`. No rules are auto-applied: no `extra.rector.includes` key is declared, so `rector.php` still opts in explicitly. diff --git a/composer.json b/composer.json index 2150831..5386520 100644 --- a/composer.json +++ b/composer.json @@ -77,7 +77,10 @@ ], "cs": "php-cs-fixer fix --dry-run --diff --config=.php-cs-fixer.php", "cs:fix": "php-cs-fixer fix --diff --config=.php-cs-fixer.php", - "mutation": "infection --threads=max", + "mutation": [ + "Composer\\Config::disableProcessTimeout", + "infection --threads=max --with-uncovered" + ], "psalm": "psalm --no-cache", "rector": "rector process --dry-run", "rector:fix": "rector process", diff --git a/infection.json5 b/infection.json5 index d5a8875..0d0ac0f 100644 --- a/infection.json5 +++ b/infection.json5 @@ -22,6 +22,24 @@ "testFramework": "testo", "minMsi": 100, "mutators": { - "@default": true + "@default": true, + // Constructor/method parameter declarations with a default value + // (`private int $x = 100,`, `bool $strict = false,`): the mutant sits on a + // signature line with no executable opcode under pcov, so it is reported Not + // Covered whatever the tests do. Only a type-prefixed `$name = default,` line + // matches; a body line cannot (no type before the variable). Do NOT add ^ or $: + // Infection wraps the pattern as ^-\s*REGEX$ over the removed diff line. + // Verified 2026-11-01 by diffing the survivor lists with and without it: + // bulkhead 6, circuit-breaker 13, json-ld 19 mutants removed, all of them + // such signature lines; Escaped and Timed Out unchanged. + "global-ignoreSourceCodeByRegex": [ + "(?:(?:private|protected|public|readonly)\\s+)*\\??[A-Za-z_\\\\][\\w\\\\|]*\\s+\\$\\w+\\s*=\\s*[^,;]+," + ], + // Off: with --with-uncovered a PublicVisibility mutant sits on a parameterless + // method's signature line, which has no executable opcode under pcov/xdebug, + // so it is reported Not Covered and can never be killed, whatever #[Covers] + // says. Measured on pinned-http-client 2026-11-01: 4 of 4 such mutants were + // Not Covered, none killed. + "PublicVisibility": false } } diff --git a/tests/DocblockTypeRewriterTest.php b/tests/DocblockTypeRewriterTest.php index 6ed81b7..c67c18f 100644 --- a/tests/DocblockTypeRewriterTest.php +++ b/tests/DocblockTypeRewriterTest.php @@ -32,6 +32,32 @@ public function rewritesReturnTags(string $docText, ?string $expected): void Assert::same((new DocblockTypeRewriter())->rewrittenReturnTag($docText), $expected); } + public function aDocblockThePatternEngineCannotSplitIsLeftAlone(): void + { + // PCRE keeps compiled patterns (JIT included) in a bounded cache and a + // JIT-compiled match ignores the backtrack limit: evict, then recompile + // without JIT so that the limit applies to the split. + for ($i = 0; $i < 5_000; ++$i) { + preg_match('/evict' . $i . '/', ''); + } + + $jit = ini_set('pcre.jit', '0'); + $limit = ini_set('pcre.backtrack_limit', '1'); + set_error_handler(static function (int $severity, string $message): never { + throw new \ErrorException($message, 0, $severity); + }); + + try { + $rewritten = (new DocblockTypeRewriter())->rewrittenVarTag("/**\n * @var DateTime\n */"); + } finally { + restore_error_handler(); + ini_set('pcre.backtrack_limit', (string) $limit); + ini_set('pcre.jit', (string) $jit); + } + + Assert::null($rewritten); + } + public static function varTagProvider(): iterable { yield 'plain type' => [