Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@
</fileset>
</copy>

<copy file="${basedir}/vendor/sebastian/file-filter/LICENSE" tofile="${basedir}/build/tmp/phar/sebastian-file-filter/LICENSE"/>
<copy todir="${basedir}/build/tmp/phar/sebastian-file-filter">
<fileset dir="${basedir}/vendor/sebastian/file-filter/src">
<include name="**/*.php" />
</fileset>
</copy>

<copy file="${basedir}/vendor/sebastian/git-state/LICENSE" tofile="${basedir}/build/tmp/phar/sebastian-git-state/LICENSE"/>
<copy todir="${basedir}/build/tmp/phar/sebastian-git-state">
<fileset dir="${basedir}/vendor/sebastian/git-state/src">
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"sebastian/diff": "^8.0.0",
"sebastian/environment": "^9.1.0",
"sebastian/exporter": "^8.0.0",
"sebastian/file-filter": "^1.0@dev",
"sebastian/git-state": "^1.0",
"sebastian/global-state": "^9.0.0",
"sebastian/object-enumerator": "^8.0.0",
Expand Down
76 changes: 74 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

68 changes: 68 additions & 0 deletions src/TextUI/Configuration/FileFilterMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\TextUI\Configuration;

use function realpath;
use SebastianBergmann\FileFilter\Builder as FilterBuilder;
use SebastianBergmann\FileFilter\Filter as FileFilter;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*/
final class FileFilterMapper
{
public function map(Source $source): FileFilter
{
return (new FilterBuilder)->build(
$this->directories($source->includeDirectories()),
Copy link
Contributor

@dantleech dantleech Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems the include directories are not "normalized" - when the config is in a sub-drectory (e.g. config/phpunit.xml):

    <source ignoreSelfDeprecations="true">
        <include>
            <directory suffix=".php">../src/</directory>
        </include>
    </source>

Results in the regex:

#^/home/daniel/www/dantleech/phpunit\-6111/config/\.\./src/(?:/|$)#

But the path needs to be normalized from phpunit-6111/config/../src tophpunit-6111/src in order for it to match the source paths.

(see Symfony's Path component for an example)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, should be handled by c9821a4.

$this->files($source->includeFiles()),
$this->directories($source->excludeDirectories()),
$this->files($source->excludeFiles()),
);
}

/**
* @return list<array{path: non-empty-string, prefix: string, suffix: string}>
*/
private function directories(FilterDirectoryCollection $directories): array
{
$result = [];

foreach ($directories as $directory) {
$path = realpath($directory->path());

$result[] = [
'path' => $path !== false ? $path : $directory->path(),
'prefix' => $directory->prefix(),
'suffix' => $directory->suffix(),
];
}

return $result;
}

/**
* @return list<non-empty-string>
*/
private function files(FilterFileCollection $files): array
{
$result = [];

foreach ($files as $file) {
$path = realpath($file->path());

$result[] = $path !== false ? $path : $file->path();
}

return $result;
}
}
31 changes: 14 additions & 17 deletions src/TextUI/Configuration/SourceFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
*/
namespace PHPUnit\TextUI\Configuration;

use SebastianBergmann\FileFilter\Filter;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*
Expand All @@ -17,38 +19,33 @@
final class SourceFilter
{
private static ?self $instance = null;

/**
* @var array<non-empty-string, true>
*/
private readonly array $map;
private readonly Filter $filter;

public static function instance(): self
{
if (self::$instance === null) {
self::$instance = new self(
(new SourceMapper)->map(
Registry::get()->source(),
),
);
if (self::$instance !== null) {
return self::$instance;
}

self::$instance = new self(
(new FileFilterMapper)->map(
Registry::get()->source(),
),
);

return self::$instance;
}

/**
* @param array<non-empty-string, true> $map
*/
public function __construct(array $map)
public function __construct(Filter $filter)
{
$this->map = $map;
$this->filter = $filter;
}

/**
* @param non-empty-string $path
*/
public function includes(string $path): bool
{
return isset($this->map[$path]);
return $this->filter->accepts($path);
}
}
59 changes: 58 additions & 1 deletion tests/unit/TextUI/SourceFilterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPUnit\Framework\Attributes\Small;

#[CoversClass(SourceFilter::class)]
#[CoversClass(FileFilterMapper::class)]
#[Small]
#[Group('textui')]
#[Group('textui/configuration')]
Expand Down Expand Up @@ -513,6 +514,62 @@ public static function provider(): array
),
),
],
'file included using directory with non-canonical path' => [
[
self::fixturePath('a/PrefixSuffix.php') => true,
],
self::createSource(
includeDirectories: FilterDirectoryCollection::fromArray(
[
new FilterDirectory(self::fixturePath('/b/../a'), '', '.php'),
],
),
),
],
'file included using file with non-canonical path' => [
[
self::fixturePath('a/PrefixSuffix.php') => true,
],
self::createSource(includeFiles: FilterFileCollection::fromArray(
[
new FilterFile(self::fixturePath('/b/../a/PrefixSuffix.php')),
],
)),
],
'file excluded using directory with non-canonical path' => [
[
self::fixturePath('a/PrefixSuffix.php') => false,
],
self::createSource(
includeDirectories: FilterDirectoryCollection::fromArray(
[
new FilterDirectory(self::fixturePath(), '', '.php'),
],
),
excludeDirectories: FilterDirectoryCollection::fromArray(
[
new FilterDirectory(self::fixturePath('/b/../a'), '', '.php'),
],
),
),
],
'file excluded using file with non-canonical path' => [
[
self::fixturePath('a/PrefixSuffix.php') => false,
],
self::createSource(
includeDirectories: FilterDirectoryCollection::fromArray(
[
new FilterDirectory(self::fixturePath(), '', '.php'),
],
),
excludeFiles: FilterFileCollection::fromArray(
[
new FilterFile(self::fixturePath('/b/../a/PrefixSuffix.php')),
],
),
),
],
'files included using same directory and different prefixes' => [
[
self::fixturePath('a/c/Suffix.php') => true,
Expand Down Expand Up @@ -561,7 +618,7 @@ public function testDeterminesWhetherFileIsIncluded(array $expectations, Source
$this->assertFileExists($file);
$this->assertSame(
$shouldInclude,
new SourceFilter((new SourceMapper)->map($source))->includes($file),
new SourceFilter((new FileFilterMapper)->map($source))->includes($file),
sprintf('expected match to return %s for: %s', json_encode($shouldInclude), $file),
);
}
Expand Down