Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TASK] Enhance FilesFinder with GlobPattern #19

Merged
merged 1 commit into from
Apr 18, 2024
Merged
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
4 changes: 2 additions & 2 deletions fractor-xml/src/XmlFileProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ public function __construct(private readonly iterable $rules)
{
}

public function canHandle(\SplFileInfo $file): bool
public function canHandle(File $file): bool
{
return $file->getExtension() === 'xml';
return $file->getFileExtension() === 'xml';
}

public function handle(File $file): void
Expand Down
2 changes: 1 addition & 1 deletion fractor/src/Application/Contract/FileProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

interface FileProcessor
{
public function canHandle(\SplFileInfo $file): bool;
public function canHandle(File $file): bool;

public function handle(File $file): void;

Expand Down
13 changes: 7 additions & 6 deletions fractor/src/Application/FractorRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,22 @@ public function __construct(private FilesFinder $fileFinder, private FilesCollec

public function run(Output $output, bool $dryRun = false): void
{
$files = $this->fileFinder->findFiles($this->configuration->getPaths(), $this->configuration->getFileExtensions());
$filePaths = $this->fileFinder->findFiles($this->configuration->getPaths(), $this->configuration->getFileExtensions());

$output->progressStart(count($files));
$output->progressStart(count($filePaths));

foreach ($filePaths as $filePath) {
$file = new File($filePath, FileSystem::read($filePath));

foreach ($files as $file) {
foreach ($this->processors as $processor) {
if (!$processor->canHandle($file)) {
$output->progressAdvance();
continue;
}

$fractorFile = new File($file->getRealPath(), FileSystem::read($file->getRealPath()));
$this->fileCollector->addFile($fractorFile);
$this->fileCollector->addFile($file);

$processor->handle($fractorFile);
$processor->handle($file);
$output->progressAdvance();
}
}
Expand Down
15 changes: 13 additions & 2 deletions fractor/src/Application/ValueObject/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ final class File
{
private bool $hasChanged = false;
private readonly string $originalContent;
private string $directoryName;
private string $fileName;
private string $fileExtension;

public function __construct(private readonly string $filePath, private string $content)
{
$this->originalContent = $this->content;
$this->directoryName = dirname($this->filePath);
$this->fileName = basename($this->filePath);
$this->fileExtension = pathinfo($this->fileName, PATHINFO_EXTENSION);
}

public function getFilePath(): string
Expand All @@ -21,19 +27,24 @@ public function getFilePath(): string

public function getDirectoryName(): string
{
return dirname($this->filePath);
return $this->directoryName;
}

public function getFileName(): string
{
return basename($this->filePath);
return $this->fileName;
}

public function getContent(): string
{
return $this->content;
}

public function getFileExtension(): string
{
return $this->fileExtension;
}

public function changeFileContent(string $newFileContent): void
{
if ($this->content === $newFileContent) {
Expand Down
30 changes: 30 additions & 0 deletions fractor/src/FileSystem/FileAndDirectoryFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace a9f\Fractor\FileSystem;

final class FileAndDirectoryFilter
{
/**
* @param string[] $filesAndDirectories
* @return string[]
*/
public function filterDirectories(array $filesAndDirectories): array
{
$directories = array_filter($filesAndDirectories, static fn (string $path): bool => is_dir($path));

return array_values($directories);
}

/**
* @param string[] $filesAndDirectories
* @return string[]
*/
public function filterFiles(array $filesAndDirectories): array
{
$files = array_filter($filesAndDirectories, static fn (string $path): bool => is_file($path));

return array_values($files);
}
}
82 changes: 71 additions & 11 deletions fractor/src/FileSystem/FilesFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,51 @@

use Symfony\Component\Finder\Finder;

final class FilesFinder
final readonly class FilesFinder
{
public function __construct(private FilesystemTweaker $filesystemTweaker, private FileAndDirectoryFilter $fileAndDirectoryFilter)
{
}

/**
* @param list<non-empty-string> $directories
* @param list<non-empty-string> $fileExtensions
* @return list<\SplFileInfo>
* @param string[] $source
* @param string[] $suffixes
* @return string[]
*/
public function findFiles(array $directories, array $fileExtensions): array
public function findFiles(array $source, array $suffixes, bool $sortByName = true): array
{
$filesAndDirectories = $this->filesystemTweaker->resolveWithFnmatch($source);

$files = $this->fileAndDirectoryFilter->filterFiles($filesAndDirectories);

$filteredFilePaths = array_filter(
$files,
fn (string $filePath): bool => true // TODO: Add skipper here
);

if ($suffixes !== []) {
$fileWithExtensionsFilter = static function (string $filePath) use ($suffixes): bool {
$filePathExtension = pathinfo($filePath, PATHINFO_EXTENSION);
return in_array($filePathExtension, $suffixes, true);
};
$filteredFilePaths = array_filter($filteredFilePaths, $fileWithExtensionsFilter);
}

$directories = $this->fileAndDirectoryFilter->filterDirectories($filesAndDirectories);
$filteredFilePathsInDirectories = $this->findInDirectories($directories, $suffixes, $sortByName);

return [...$filteredFilePaths, ...$filteredFilePathsInDirectories];
}

/**
* @param string[] $directories
* @param string[] $suffixes
* @return string[]
*/
private function findInDirectories(array $directories, array $suffixes, bool $sortByName = true): array
{
if ($directories === []) {
throw new \UnexpectedValueException('Directories must not be an empty array');
return [];
}

$finder = Finder::create()
Expand All @@ -23,14 +57,40 @@ public function findFiles(array $directories, array $fileExtensions): array
->size('> 0')
->in($directories);

foreach ($fileExtensions as $fileExtension) {
$finder->name('*.' . $fileExtension);
if ($sortByName) {
$finder->sortByName();
}

if ($suffixes !== []) {
$suffixesPattern = $this->normalizeSuffixesToPattern($suffixes);
$finder->name($suffixesPattern);
}

$files = [];
$filePaths = [];
foreach ($finder as $fileInfo) {
$files[] = $fileInfo;
// getRealPath() function will return false when it checks broken symlinks.
// So we should check if this file exists or we got broken symlink

/** @var string|false $path */
$path = $fileInfo->getRealPath();
if ($path === false) {
continue;
}

// TODO: Add skipper here

$filePaths[] = $path;
}
return $files;

return $filePaths;
}

/**
* @param string[] $suffixes
*/
private function normalizeSuffixesToPattern(array $suffixes): string
{
$suffixesPattern = implode('|', $suffixes);
return '#\.(' . $suffixesPattern . ')$#';
}
}
41 changes: 41 additions & 0 deletions fractor/src/FileSystem/FilesystemTweaker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace a9f\Fractor\FileSystem;

final class FilesystemTweaker
{
/**
* This will turn paths like "src/Symfony/Component/*\/Tests" to existing directory paths
*
* @param string[] $paths
*
* @return string[]
*/
public function resolveWithFnmatch(array $paths): array
{
$absolutePathsFound = [];
foreach ($paths as $path) {
if (\str_contains($path, '*')) {
$foundPaths = $this->foundInGlob($path);
$absolutePathsFound = [...$absolutePathsFound, ...$foundPaths];
} else {
$absolutePathsFound[] = $path;
}
}

return $absolutePathsFound;
}

/**
* @return string[]
*/
private function foundInGlob(string $path): array
{
/** @var string[] $paths */
$paths = (array) glob($path);

return array_filter($paths, static fn (string $path): bool => file_exists($path));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace a9f\Fractor\Tests\FileSystem\FileAndDirectoryFilter;

use a9f\Fractor\FileSystem\FileAndDirectoryFilter;
use PHPUnit\Framework\TestCase;

final class FileAndDirectoryFilterTest extends TestCase
{
private FileAndDirectoryFilter $fileAndDirectoryFilter;

protected function setUp(): void
{
$this->fileAndDirectoryFilter = new FileAndDirectoryFilter();
}

public function testSeparateFilesAndDirectories(): void
{
$sources = [__DIR__, __DIR__ . '/FileAndDirectoryFilterTest.php'];

$files = $this->fileAndDirectoryFilter->filterFiles($sources);
$directories = $this->fileAndDirectoryFilter->filterDirectories($sources);

self::assertCount(1, $files);
self::assertCount(1, $directories);

self::assertSame($files, [__DIR__ . '/FileAndDirectoryFilterTest.php']);
self::assertSame($directories, [__DIR__]);
}
}
64 changes: 64 additions & 0 deletions fractor/tests/FileSystem/FilesFinder/FilesFinderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

namespace a9f\Fractor\Tests\FileSystem\FilesFinder;

use a9f\Fractor\FileSystem\FilesFinder;
use a9f\Fractor\Testing\PHPUnit\AbstractFractorTestCase;
use PHPUnit\Framework\Attributes\Test;

final class FilesFinderTest extends AbstractFractorTestCase
{
private FilesFinder $subject;

protected function setUp(): void
{
parent::setUp();
$this->subject = $this->getService(FilesFinder::class);
}

#[Test]
public function findAllNonEmptyFilesInGivenDirectories(): void
{
self::assertCount(4, $this->subject->findFiles([__DIR__ . '/Fixtures/Source'], []));
}

#[Test]
public function findAllNonEmptyFilesInGivenDirectoriesWithGivenExtensions(): void
{
self::assertCount(2, $this->subject->findFiles([__DIR__ . '/Fixtures/Source'], ['txt', 'json']));
}

#[Test]
public function withFollowingBrokenSymlinks(): void
{
$foundFiles = $this->subject->findFiles([__DIR__ . '/Fixtures/SourceWithBrokenSymlinks'], []);
self::assertCount(0, $foundFiles);
}

#[Test]
public function directoriesWithGlobPattern(): void
{
$foundDirectories = $this->subject->findFiles([__DIR__ . '/Fixtures/SourceWithSubFolders/folder*/*'], []);
self::assertCount(2, $foundDirectories);
}

#[Test]
public function filesWithGlobPattern(): void
{
$foundFiles = $this->subject->findFiles([__DIR__ . '/Fixtures/SourceWithSubFolders/**/foo.txt'], ['txt']);
self::assertCount(2, $foundFiles);

/** @var string $foundFile */
$foundFile = array_pop($foundFiles);

$fileBasename = $this->getFileBasename($foundFile);
self::assertSame('foo.txt', $fileBasename);
}

private function getFileBasename(string $foundFile): string
{
return pathinfo($foundFile, PATHINFO_BASENAME);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Loading