diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml
index c405ce3..92750b1 100644
--- a/.github/workflows/checks.yml
+++ b/.github/workflows/checks.yml
@@ -47,7 +47,9 @@ jobs:
php-version: ${{ matrix.php-version }}
-
name: Update dependencies
- run: composer update --no-progress --${{ matrix.dependency-version }} --prefer-dist --no-interaction
+ run: |
+ composer remove --dev shipmonk/coverage-guard --no-update
+ composer update --no-progress --${{ matrix.dependency-version }} --prefer-dist --no-interaction
-
name: Run tests
run: composer check:tests
diff --git a/composer.json b/composer.json
index d7234a6..d8d3dc1 100644
--- a/composer.json
+++ b/composer.json
@@ -22,6 +22,7 @@
"phpunit/phpunit": "^9.6.22",
"shipmonk/coding-standard": "^0.2.0",
"shipmonk/composer-dependency-analyser": "^1.8.1",
+ "shipmonk/coverage-guard": "dev-master",
"shipmonk/dead-code-detector": "^0.9.0",
"shipmonk/name-collision-detector": "^2.1.1",
"shipmonk/phpstan-dev": "^0.1.2"
@@ -59,7 +60,7 @@
"@check:ec",
"@check:cs",
"@check:types",
- "@check:tests",
+ "@check:coverage",
"@check:dependencies",
"@check:collisions",
"@check:ignores"
@@ -69,11 +70,15 @@
"composer normalize --dry-run --no-check-lock --no-update-lock",
"composer validate --strict"
],
+ "check:coverage": [
+ "XDEBUG_MODE=coverage phpunit tests --coverage-clover cache/clover.xml",
+ "coverage-guard check cache/clover.xml"
+ ],
"check:cs": "phpcs",
"check:dependencies": "composer-dependency-analyser",
"check:ec": "ec src tests",
"check:ignores": "php bin/verify-inline-ignore.php",
- "check:tests": "phpunit -vvv tests",
+ "check:tests": "phpunit tests",
"check:types": "phpstan analyse -vv --ansi",
"fix:cs": "phpcbf"
}
diff --git a/composer.lock b/composer.lock
index bc4eadb..2d1ffe3 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "e45e2400e8fe0857f5bf825391d73768",
+ "content-hash": "41d8331fa766c41e8117616a6f039fab",
"packages": [
{
"name": "phpstan/phpstan",
@@ -3086,6 +3086,67 @@
},
"time": "2025-11-25T14:38:16+00:00"
},
+ {
+ "name": "shipmonk/coverage-guard",
+ "version": "dev-master",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/shipmonk-rnd/coverage-guard.git",
+ "reference": "2f1281f7883ce7933ee029241fd517bb14cafcd6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/shipmonk-rnd/coverage-guard/zipball/2f1281f7883ce7933ee029241fd517bb14cafcd6",
+ "reference": "2f1281f7883ce7933ee029241fd517bb14cafcd6",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.19.1 || ^5.0",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "editorconfig-checker/editorconfig-checker": "10.7.0",
+ "ergebnis/composer-normalize": "2.48.2",
+ "phpstan/phpstan": "2.1.32",
+ "phpstan/phpstan-phpunit": "2.0.7",
+ "phpstan/phpstan-strict-rules": "2.0.7",
+ "phpunit/php-code-coverage": "^10.1",
+ "phpunit/phpunit": "~10.5.58",
+ "sebastian/diff": "^4.0 || ^5.0 || ^6.0 || ^7.0",
+ "shipmonk/coding-standard": "^0.2.0",
+ "shipmonk/composer-dependency-analyser": "1.8.3",
+ "shipmonk/dead-code-detector": "0.13.3",
+ "shipmonk/name-collision-detector": "2.1.1",
+ "shipmonk/phpstan-rules": "4.2.1"
+ },
+ "default-branch": true,
+ "bin": [
+ "bin/coverage-guard"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ShipMonk\\CoverageGuard\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Enforce code coverage in your CI. Not by percentage, but target core methods. No more untested Facades, Controllers, or Repositories. Allows you to start enforcing coverage for new code only!",
+ "keywords": [
+ "code coverage",
+ "git diff",
+ "patch",
+ "phpunit",
+ "testing"
+ ],
+ "support": {
+ "issues": "https://github.com/shipmonk-rnd/coverage-guard/issues",
+ "source": "https://github.com/shipmonk-rnd/coverage-guard/tree/master"
+ },
+ "time": "2025-11-28T14:42:05+00:00"
+ },
{
"name": "shipmonk/dead-code-detector",
"version": "0.9.2",
@@ -3464,7 +3525,9 @@
],
"aliases": [],
"minimum-stability": "stable",
- "stability-flags": {},
+ "stability-flags": {
+ "shipmonk/coverage-guard": 20
+ },
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
diff --git a/coverage-guard.php b/coverage-guard.php
new file mode 100644
index 0000000..e48976f
--- /dev/null
+++ b/coverage-guard.php
@@ -0,0 +1,12 @@
+addRule(new EnforceCoverageForMethodsRule(
+ requiredCoveragePercentage: 70,
+ minExecutableLines: 5,
+));
+
+return $config;
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index b12349e..caa06c0 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -14,4 +14,10 @@
+
+
+
+ src
+
+
diff --git a/tests/Rule/ForbidCheckedExceptionInCallableRuleTest.php b/tests/Rule/ForbidCheckedExceptionInCallableRuleTest.php
index 086f750..be6364e 100644
--- a/tests/Rule/ForbidCheckedExceptionInCallableRuleTest.php
+++ b/tests/Rule/ForbidCheckedExceptionInCallableRuleTest.php
@@ -49,7 +49,8 @@ protected function getRule(): Rule
'ForbidCheckedExceptionInCallableRule\FirstClassCallableTest::allowThrow' => [1],
'ForbidCheckedExceptionInCallableRule\ArrowFunctionTest::allowThrow' => [0],
'ForbidCheckedExceptionInCallableRule\ArrowFunctionTest::__construct' => [0],
- 'allowed_function' => [0],
+ 'ForbidCheckedExceptionInCallableRule\allowed_function' => [0], // not really needed as functions are always considered immediately invoked (https://phpstan.org/writing-php-code/phpdocs-basics#callables)
+ 'ForbidCheckedExceptionInCallableRule\allowed_function_not_immediate' => [0],
],
);
}
diff --git a/tests/Rule/data/ForbidCheckedExceptionInCallableRule/code.php b/tests/Rule/data/ForbidCheckedExceptionInCallableRule/code.php
index 786ba72..16e5786 100644
--- a/tests/Rule/data/ForbidCheckedExceptionInCallableRule/code.php
+++ b/tests/Rule/data/ForbidCheckedExceptionInCallableRule/code.php
@@ -11,6 +11,11 @@ function throwing_function() {}
function allowed_function(callable $callable) {}
+/**
+ * @param-later-invoked-callable $callable
+ */
+function allowed_function_not_immediate(callable $callable) {}
+
interface CallableTest {
public function allowThrowInInterface(callable $callable): void;
@@ -96,6 +101,12 @@ public function testPassedCallbacksA5(): void
$this->allowThrow(42, throwing_function(...));
}
+ public function testPassedCallbacksA6(): void
+ {
+ allowed_function(throwing_function(...));
+ allowed_function_not_immediate(throwing_function(...));
+ }
+
public function testPassedCallbacksA6(): void
{
$this->immediateThrow(
@@ -510,3 +521,5 @@ private function throws(): void
throw new CheckedException();
}
}
+
+