Repository: Codeception/Codeception (language: php)

Issue:
coverage: whitelist: exclude option has no effect since PHPUnit 11.0
```bash
codecept run  --coverage-html 
```
with  configuration 
```bash
coverage:
    enabled: true
    exclude:
        - vendor/*
```
causes warning 
```bash 
WARNING: `coverage: whitelist: exclude` option has no effect since PHPUnit 11.0
```
and vendor directory is not excluded

### Details
I found changelog https://github.com/sebastianbergmann/phpunit/blob/9.3.0/ChangeLog-9.3.md#930---2020-08-07 with comment: 
"Using --whitelist <directory> to include a directory in code coverage reports is now deprecated, please use --coverage-filter <directory> instead"
Is documentation on https://codeception.com/docs/Codecoverage outdated?

* Codeception version: 5.1.0
* PHP Version: 8.2
* Operating System: Win
* Installation type: Composer



Discussion / hints:
Excluding is no longer supported by phpunit/php-code-coverage library, you must be more specific about what code is included.
At a second glance, implementation of exclude is not that difficult.
Thank you. Exclude is very useful feature, because  coverage vendor is useless and often slow. 

Fix patch:
diff --git a/src/Codeception/Coverage/Filter.php b/src/Codeception/Coverage/Filter.php
index be21aca2fd..f572936ad0 100644
--- a/src/Codeception/Coverage/Filter.php
+++ b/src/Codeception/Coverage/Filter.php
@@ -7,8 +7,9 @@
 use Codeception\Configuration;
 use Codeception\Exception\ConfigurationException;
 use Codeception\Exception\ModuleException;
-use Codeception\Lib\Notification;
+use PHPUnit\Runner\Version as PHPUnitVersion;
 use SebastianBergmann\CodeCoverage\CodeCoverage;
+use SebastianBergmann\CodeCoverage\Filter as PhpUnitFilter;
 use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
 use Symfony\Component\Finder\Finder;
 
@@ -16,24 +17,24 @@
 use function explode;
 use function implode;
 use function is_array;
-use function method_exists;
+use function iterator_to_array;
 use function str_replace;
 
 class Filter
 {
-    protected static ?Filter $c3 = null;
+    protected static ?self $codeceptionFilter = null;
 
-    protected ?\SebastianBergmann\CodeCoverage\Filter $filter = null;
+    protected ?PhpUnitFilter $phpUnitFilter = null;
 
     public function __construct(protected ?CodeCoverage $phpCodeCoverage)
     {
-        $this->filter = $this->phpCodeCoverage->filter();
+        $this->phpUnitFilter = $this->phpCodeCoverage->filter();
     }
 
     public static function setup(CodeCoverage $phpCoverage): self
     {
-        self::$c3 = new self($phpCoverage);
-        return self::$c3;
+        self::$codeceptionFilter = new self($phpCoverage);
+        return self::$codeceptionFilter;
     }
 
     /**
@@ -41,7 +42,7 @@ public static function setup(CodeCoverage $phpCoverage): self
      */
     public function whiteList(array $config): self
     {
-        $filter = $this->filter;
+        $filter = $this->phpUnitFilter;
         if (!isset($config['coverage'])) {
             return $this;
         }
@@ -56,6 +57,10 @@ public function whiteList(array $config): self
             }
         }
 
+        if (PHPUnitVersion::series() >= 11) {
+            return $this->newWhiteList($coverage['whitelist']);
+        }
+
         if (isset($coverage['whitelist']['include'])) {
             if (!is_array($coverage['whitelist']['include'])) {
                 throw new ConfigurationException('Error parsing yaml. Config `whitelist: include:` should be an array');
@@ -75,27 +80,77 @@ public function whiteList(array $config): self
             if (!is_array($coverage['whitelist']['exclude'])) {
                 throw new ConfigurationException('Error parsing yaml. Config `whitelist: exclude:` should be an array');
             }
-            if (method_exists($filter, 'excludeFile')) {
-                foreach ($coverage['whitelist']['exclude'] as $fileOrDir) {
-                    try {
-                        $finder = !str_contains($fileOrDir, '*')
-                            ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
-                            : $this->matchWildcardPattern($fileOrDir);
-
-                        foreach ($finder as $file) {
-                            $filter->excludeFile((string)$file);
-                        }
-                    } catch (DirectoryNotFoundException) {
-                        continue;
+
+            foreach ($coverage['whitelist']['exclude'] as $fileOrDir) {
+                try {
+                    $finder = !str_contains($fileOrDir, '*')
+                        ? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
+                        : $this->matchWildcardPattern($fileOrDir);
+
+                    foreach ($finder as $file) {
+                        $filter->excludeFile((string)$file);
                     }
+                } catch (DirectoryNotFoundException) {
+                    continue;
                 }
-            } else {
-                Notification::warning('`coverage: whitelist: exclude` option has no effect since PHPUnit 11.0', '');
             }
         }
         return $this;
     }
 
+    private function newWhiteList(array $whitelist): self
+    {
+        $include = $whitelist['include'] ?? [];
+        $exclude = $whitelist['exclude'] ?? [];
+
+        if (!is_array($include)) {
+            throw new ConfigurationException('Error parsing yaml. Config `whitelist: include:` should be an array');
+        }
+        if (!is_array($exclude)) {
+            throw new ConfigurationException('Error parsing yaml. Config `whitelist: exclude:` should be an array');
+        }
+
+        if (count($exclude) === 0 && count($include) === 0) {
+            return $this;
+        }
+
+        if (count($include) === 0) {
+            $include = [
+                Configuration::projectDir() . DIRECTORY_SEPARATOR . '*'
+            ];
+        }
+
+        $allIncludedFiles = [];
+        foreach ($include as $fileOrDir) {
+            $finder = !str_contains($fileOrDir, '*')
+                ? $this->matchFileOrDirectory($fileOrDir)
+                : $this->matchWildcardPattern($fileOrDir);
+
+            $allIncludedFiles += iterator_to_array($finder->getIterator());
+        }
+
+        $allExcludedFiles = [];
+        foreach ($exclude as $fileOrDir) {
+            try {
+                $finder = !str_contains($fileOrDir, '*')
+                    ? $this->matchFileOrDirectory($fileOrDir)
+                    : $this->matchWildcardPattern($fileOrDir);
+
+                $allExcludedFiles += iterator_to_array($finder->getIterator());
+            } catch (DirectoryNotFoundException) {
+                continue;
+            }
+        }
+
+        $coveredFiles = array_diff($allIncludedFiles, $allExcludedFiles);
+
+        foreach ($coveredFiles as $coveredFile) {
+            $this->phpUnitFilter->includeFile((string)$coveredFile);
+        }
+
+        return $this;
+    }
+
     /**
      * @throws ModuleException
      */
@@ -108,6 +163,21 @@ public function blackList(array $config): self
         return $this;
     }
 
+    private function matchFileOrDirectory(string $fileOrDir): Finder
+    {
+        $fullPath = Configuration::projectDir() . $fileOrDir;
+        $finder = Finder::create();
+        if (is_dir($fullPath)) {
+            $finder->in($fullPath);
+            $finder->name('*.php');
+        } else {
+            $finder->in(dirname($fullPath));
+            $finder->name(basename($fullPath));
+        }
+        $finder->ignoreVCS(true)->files();
+        return $finder;
+    }
+
     protected function matchWildcardPattern(string $pattern): Finder
     {
         $finder = Finder::create();
@@ -123,15 +193,15 @@ protected function matchWildcardPattern(string $pattern): Finder
             if ($lastPath === '*') {
                 $finder->in(Configuration::projectDir() . implode('/', $parts));
             } else {
-                $finder->in(Configuration::projectDir() . implode('/', $parts) . '/' . $lastPath);
+                $finder->in(Configuration::projectDir() . implode('/', [...$parts, $lastPath]));
             }
         }
         $finder->ignoreVCS(true)->files();
         return $finder;
     }
 
-    public function getFilter(): \SebastianBergmann\CodeCoverage\Filter
+    public function getFilter(): PhpUnitFilter
     {
-        return $this->filter;
+        return $this->phpUnitFilter;
     }
 }


Test patch:
diff --git a/tests/unit/Codeception/Coverage/FilterTest.php b/tests/unit/Codeception/Coverage/FilterTest.php
index 183a794641..7f5031d182 100644
--- a/tests/unit/Codeception/Coverage/FilterTest.php
+++ b/tests/unit/Codeception/Coverage/FilterTest.php
@@ -23,9 +23,6 @@ protected function _before()
 
     public function testWhitelistFilterApplied()
     {
-        if (version_compare(VersionAlias::id(), '11.0', '>=')) {
-            $this->markTestSkipped('Whitelist exclude option has no effect since PHPUnit 11.0');
-        }
         $config = [
             'coverage' => [
                 'whitelist' => [
@@ -57,10 +54,6 @@ public function testWhitelistFilterApplied()
 
     public function testShortcutFilter()
     {
-        if (version_compare(VersionAlias::id(), '11.0', '>=')) {
-            $this->markTestSkipped('Whitelist exclude option has no effect since PHPUnit 11.0');
-        }
-
         $config = ['coverage' => [
             'include' => ['tests/*'],
             'exclude' => ['tests/support/CodeGuy.php']