diff --git a/packages/filesystem/composer.json b/packages/filesystem/composer.json index fe9ec6e9c..a41c63aba 100644 --- a/packages/filesystem/composer.json +++ b/packages/filesystem/composer.json @@ -24,7 +24,8 @@ }, "minimum-stability": "stable", "require": { - "php": "^8.1" + "php": "^8.1", + "webmozart/assert": "^1.3" }, "extra": { "branch-alias": { diff --git a/packages/filesystem/src/Finder/Exclude.php b/packages/filesystem/src/Finder/Exclude.php new file mode 100644 index 000000000..630409eaa --- /dev/null +++ b/packages/filesystem/src/Finder/Exclude.php @@ -0,0 +1,72 @@ + */ + private readonly array $paths; + + /** @param list $paths */ + public function __construct( + array $paths = [], + private readonly bool $hidden = false, + private readonly bool $symlinks = false, + ) { + $this->paths = array_values( + array_map( + static function (string|Path $path): string { + if (str_starts_with((string) $path, '/')) { + return (string) $path; + } + + return '/' . $path; + }, + $paths, + ), + ); + } + + /** @return list */ + public function getPaths(): array + { + return $this->paths; + } + + public function excludeHidden(): bool + { + return $this->hidden; + } + + public function followSymlinks(): bool + { + return $this->symlinks; + } + + /** @param list $excludePaths */ + public function withPaths(array $excludePaths): self + { + return new self( + $excludePaths, + $this->hidden, + $this->symlinks, + ); + } +} diff --git a/packages/filesystem/src/Finder/SpecificationFactory.php b/packages/filesystem/src/Finder/SpecificationFactory.php new file mode 100644 index 000000000..a1b05f9d9 --- /dev/null +++ b/packages/filesystem/src/Finder/SpecificationFactory.php @@ -0,0 +1,83 @@ + $paths + * @param list $extensions + */ + public function create(array $paths, Exclude $ignore, array $extensions): SpecificationInterface + { + /** @var ?Glob $pathSpec */ + $pathSpec = null; + foreach ($paths as $path) { + if ($path instanceof Path) { + $condition = new InPath(new FlyFinderPath((string) $path)); + } else { + $condition = new Glob($path); + } + + if ($pathSpec === null) { + $pathSpec = $condition; + continue; + } + + $pathSpec = $pathSpec->orSpecification($condition); + } + + /** @var ?Glob $ignoreSpec */ + $ignoreSpec = null; + foreach ($ignore->getPaths() as $path) { + if ($ignoreSpec === null) { + $ignoreSpec = new Glob($path); + continue; + } + + $ignoreSpec = $ignoreSpec->orSpecification(new Glob($path)); + } + + if ($ignore->excludeHidden()) { + $ignoreSpec = $ignoreSpec === null + ? new IsHidden() + : $ignoreSpec->orSpecification(new IsHidden()); + } + + $result = new HasExtension($extensions); + if ($ignoreSpec !== null) { + $result = $result->andSpecification(new NotSpecification($ignoreSpec)); + } + + if ($pathSpec !== null) { + $result = $result->andSpecification($pathSpec); + } + + return $result; + } +} diff --git a/packages/filesystem/src/Finder/SpecificationFactoryInterface.php b/packages/filesystem/src/Finder/SpecificationFactoryInterface.php new file mode 100644 index 000000000..dccffdebd --- /dev/null +++ b/packages/filesystem/src/Finder/SpecificationFactoryInterface.php @@ -0,0 +1,31 @@ + $paths + * @param list $extensions + */ + public function create(array $paths, Exclude $ignore, array $extensions): SpecificationInterface; +} diff --git a/packages/filesystem/src/Path.php b/packages/filesystem/src/Path.php new file mode 100644 index 000000000..bbf4b51e3 --- /dev/null +++ b/packages/filesystem/src/Path.php @@ -0,0 +1,90 @@ +path === (string) $otherPath; + } + + /** + * returns a string representation of the path. + */ + public function __toString(): string + { + return $this->path; + } + + /** + * Returns whether the file path is an absolute path. + * + * @param string $file A file path + */ + public static function isAbsolutePath(string $file): bool + { + return strspn($file, '/\\', 0, 1) + || (strlen($file) > 3 && ctype_alpha($file[0]) + && $file[1] === ':' + && strspn($file, '/\\', 2, 1) + ) + || parse_url($file, PHP_URL_SCHEME) !== null; + } + + public static function dirname(Path $input): self + { + $parts = explode('/', (string) $input); + array_pop($parts); + + $path = implode('/', $parts); + if ($path === '') { + return new self('/'); + } + + return new self($path); + } +} diff --git a/packages/filesystem/tests/unit/Finder/SpecificationFactoryTest.php b/packages/filesystem/tests/unit/Finder/SpecificationFactoryTest.php new file mode 100644 index 000000000..6ddaaffc5 --- /dev/null +++ b/packages/filesystem/tests/unit/Finder/SpecificationFactoryTest.php @@ -0,0 +1,134 @@ +fixture = new SpecificationFactory(); + } + + public function testCreateIgnoreHidden(): void + { + $specification = $this->fixture->create( + ['/some/path/**/*', '/a/second/path/**/*'], + new Exclude(hidden: true), + ['php', 'php3'], + ); + + $this->assertEquals( + new AndSpecification( + new AndSpecification( + new HasExtension(['php', 'php3']), + new NotSpecification( + new IsHidden(), + ), + ), + new OrSpecification( + new Glob('/some/path/**/*'), + new Glob('/a/second/path/**/*'), + ), + ), + $specification, + ); + } + + public function testCreateIgnorePath(): void + { + $specification = $this->fixture->create( + ['/src/'], + new Exclude(['/src/some/path', '/src/some/other/path']), + ['php'], + ); + + $this->assertEquals( + new AndSpecification( + new AndSpecification( + new HasExtension(['php']), + new NotSpecification( + new OrSpecification( + new Glob('/src/some/path'), + new Glob('/src/some/other/path'), + ), + ), + ), + new Glob('/src/'), + ), + $specification, + ); + } + + public function testNoPaths(): void + { + $specification = $this->fixture->create([], new Exclude(['/src/some/path']), ['php']); + + $this->assertEquals( + new AndSpecification( + new HasExtension(['php']), + new NotSpecification( + new Glob('/src/some/path'), + ), + ), + $specification, + ); + } + + public function testNoIgnore(): void + { + $specification = $this->fixture->create(['/src/'], new Exclude(), ['php']); + + $this->assertEquals( + new AndSpecification( + new HasExtension(['php']), + new Glob('/src/'), + ), + $specification, + ); + } + + public function testInPathMustBeOfTheTypeString(): void + { + $specification = $this->fixture->create( + [ + '/PHPCompatibility/*', + '/PHPCompatibility/Sniffs/', + ], + new Exclude(), + ['php'], + ); + + $this->assertEquals( + new AndSpecification( + new HasExtension(['php']), + new OrSpecification( + new Glob('/PHPCompatibility/*'), + new Glob('/PHPCompatibility/Sniffs/'), + ), + ), + $specification, + ); + } +} diff --git a/packages/filesystem/tests/unit/PathTest.php b/packages/filesystem/tests/unit/PathTest.php new file mode 100644 index 000000000..8b0fc0d00 --- /dev/null +++ b/packages/filesystem/tests/unit/PathTest.php @@ -0,0 +1,58 @@ +assertSame('/my/Path', (string) $path); + } + + public function testItCanCompareItselfToAnotherPath(): void + { + $subject = new Path('a'); + $similar = new Path('a'); + $dissimilar = new Path('b'); + + $this->assertTrue($subject->equals($similar)); + $this->assertFalse($subject->equals($dissimilar)); + } + + public function testItCanCheckWhetherTheGivenPathIsAnAbsolutePath(): void + { + $this->assertTrue(Path::isAbsolutePath('\\\\my\\absolute\\path')); + $this->assertTrue(Path::isAbsolutePath('/my/absolute/path')); + $this->assertTrue(Path::isAbsolutePath('c:\\my\\absolute\\path')); + $this->assertTrue(Path::isAbsolutePath('http://my/absolute/path')); + $this->assertTrue(Path::isAbsolutePath('//my/absolute/path')); + + $this->assertFalse(Path::isAbsolutePath('path')); + $this->assertFalse(Path::isAbsolutePath('my/absolute/path')); + $this->assertFalse(Path::isAbsolutePath('./my/absolute/path')); + $this->assertFalse(Path::isAbsolutePath('../my/absolute/path')); + } + + public function testDirnameOnRootFile(): void + { + $path = Path::dirname(new Path('/config.xml')); + + $this->assertEquals(new Path('/'), $path); + } +} diff --git a/packages/guides-cli/resources/config/services.php b/packages/guides-cli/resources/config/services.php index 2c58983c8..e069a707c 100644 --- a/packages/guides-cli/resources/config/services.php +++ b/packages/guides-cli/resources/config/services.php @@ -4,6 +4,7 @@ use Monolog\Logger; use phpDocumentor\Guides\Cli\Application; +use phpDocumentor\Guides\Cli\Command\ProgressBarSubscriber; use phpDocumentor\Guides\Cli\Command\Run; use phpDocumentor\Guides\Cli\Command\WorkingDirectorySwitcher; use Psr\Clock\ClockInterface; @@ -41,5 +42,7 @@ ->public() ->set(WorkingDirectorySwitcher::class) - ->tag('event_listener', ['event' => ConsoleEvents::COMMAND, 'method' => '__invoke']); + ->tag('event_listener', ['event' => ConsoleEvents::COMMAND, 'method' => '__invoke']) + + ->set(ProgressBarSubscriber::class); }; diff --git a/packages/guides-cli/resources/schema/guides.xsd b/packages/guides-cli/resources/schema/guides.xsd index 9b1d06af7..59cb6c343 100644 --- a/packages/guides-cli/resources/schema/guides.xsd +++ b/packages/guides-cli/resources/schema/guides.xsd @@ -6,26 +6,28 @@ version="3.0" elementFormDefault="qualified" > + - + + - - + + - + @@ -36,6 +38,14 @@ + + + + + + + + diff --git a/packages/guides-cli/src/Command/ProgressBarSubscriber.php b/packages/guides-cli/src/Command/ProgressBarSubscriber.php new file mode 100644 index 000000000..eee8bd2b3 --- /dev/null +++ b/packages/guides-cli/src/Command/ProgressBarSubscriber.php @@ -0,0 +1,117 @@ +registerParserProgressBar($output, $dispatcher); + $this->registerRenderProgressBar($output, $dispatcher); + } + + private function registerParserProgressBar(ConsoleOutputInterface $output, EventDispatcherInterface $dispatcher): void + { + $parsingProgressBar = new ProgressBar($output->section()); + $parsingProgressBar->setFormat('Parsing: %current%/%max% [%bar%] %percent:3s%% %message%'); + $parsingStartTime = microtime(true); + $dispatcher->addListener( + PostCollectFilesForParsingEvent::class, + static function (PostCollectFilesForParsingEvent $event) use ($parsingProgressBar, &$parsingStartTime): void { + // Each File needs to be first parsed then rendered + $parsingStartTime = microtime(true); + $parsingProgressBar->setMaxSteps(count($event->getFiles())); + }, + ); + $dispatcher->addListener( + PreParseDocument::class, + static function (PreParseDocument $event) use ($parsingProgressBar): void { + $parsingProgressBar->setMessage('Parsing file: ' . $event->getFileName()); + $parsingProgressBar->display(); + }, + ); + $dispatcher->addListener( + PostParseDocument::class, + static function (PostParseDocument $event) use ($parsingProgressBar): void { + $parsingProgressBar->advance(); + }, + ); + $dispatcher->addListener( + PostParseProcess::class, + static function (PostParseProcess $event) use ($parsingProgressBar, $parsingStartTime): void { + $parsingTimeElapsed = microtime(true) - $parsingStartTime; + $parsingProgressBar->setMessage(sprintf( + 'Parsed %s files in %.2f seconds', + $parsingProgressBar->getMaxSteps(), + $parsingTimeElapsed, + )); + $parsingProgressBar->finish(); + }, + ); + } + + private function registerRenderProgressBar(ConsoleOutputInterface $output, EventDispatcherInterface $dispatcher): void + { + $dispatcher->addListener( + PreRenderProcess::class, + static function (PreRenderProcess $event) use ($dispatcher, $output): void { + $renderingProgressBar = new ProgressBar($output->section(), count($event->getCommand()->getDocumentArray())); + $renderingProgressBar->setFormat('Rendering: %current%/%max% [%bar%] %percent:3s%% Output format ' . $event->getCommand()->getOutputFormat() . ': %message%'); + $renderingStartTime = microtime(true); + $dispatcher->addListener( + PreRenderDocument::class, + static function (PreRenderDocument $event) use ($renderingProgressBar): void { + $renderingProgressBar->setMessage('Rendering: ' . $event->getCommand()->getFileDestination()); + $renderingProgressBar->display(); + }, + ); + $dispatcher->addListener( + PostRenderDocument::class, + static function (PostRenderDocument $event) use ($renderingProgressBar): void { + $renderingProgressBar->advance(); + }, + ); + $dispatcher->addListener( + PostRenderProcess::class, + static function (PostRenderProcess $event) use ($renderingProgressBar, $renderingStartTime): void { + $renderingElapsedTime = microtime(true) - $renderingStartTime; + $renderingProgressBar->setMessage(sprintf( + 'Rendered %s documents in %.2f seconds', + $renderingProgressBar->getMaxSteps(), + $renderingElapsedTime, + )); + $renderingProgressBar->finish(); + }, + ); + }, + ); + } +} diff --git a/packages/guides-cli/src/Command/Run.php b/packages/guides-cli/src/Command/Run.php index 0efa85793..aeebc66ca 100644 --- a/packages/guides-cli/src/Command/Run.php +++ b/packages/guides-cli/src/Command/Run.php @@ -13,6 +13,7 @@ namespace phpDocumentor\Guides\Cli\Command; +use Doctrine\Deprecations\Deprecation; use Flyfinder\Path; use Flyfinder\Specification\InPath; use Flyfinder\Specification\NotSpecification; @@ -22,18 +23,11 @@ use Monolog\Handler\ErrorLogHandler; use Monolog\Handler\StreamHandler; use Monolog\Logger; +use phpDocumentor\FileSystem\Finder\Exclude; use phpDocumentor\FileSystem\FlySystemAdapter; use phpDocumentor\Guides\Cli\Logger\SpyProcessor; use phpDocumentor\Guides\Compiler\CompilerContext; -use phpDocumentor\Guides\Event\PostCollectFilesForParsingEvent; -use phpDocumentor\Guides\Event\PostParseDocument; -use phpDocumentor\Guides\Event\PostParseProcess; use phpDocumentor\Guides\Event\PostProjectNodeCreated; -use phpDocumentor\Guides\Event\PostRenderDocument; -use phpDocumentor\Guides\Event\PostRenderProcess; -use phpDocumentor\Guides\Event\PreParseDocument; -use phpDocumentor\Guides\Event\PreRenderDocument; -use phpDocumentor\Guides\Event\PreRenderProcess; use phpDocumentor\Guides\Handlers\CompileDocumentsCommand; use phpDocumentor\Guides\Handlers\ParseDirectoryCommand; use phpDocumentor\Guides\Handlers\ParseFileCommand; @@ -46,7 +40,6 @@ use Psr\Log\LogLevel; use RuntimeException; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -63,7 +56,7 @@ use function implode; use function is_countable; use function is_dir; -use function microtime; +use function method_exists; use function pathinfo; use function sprintf; use function strtoupper; @@ -77,6 +70,7 @@ public function __construct( private readonly SettingsManager $settingsManager, private readonly ClockInterface $clock, private readonly EventDispatcher $eventDispatcher, + private readonly ProgressBarSubscriber $progressBarSubscriber, ) { parent::__construct('run'); @@ -154,78 +148,15 @@ public function __construct( ); } + /** @deprecated this method will be removed in v2 */ public function registerProgressBar(ConsoleOutputInterface $output): void { - $parsingProgressBar = new ProgressBar($output->section()); - $parsingProgressBar->setFormat('Parsing: %current%/%max% [%bar%] %percent:3s%% %message%'); - $parsingStartTime = microtime(true); - $this->eventDispatcher->addListener( - PostCollectFilesForParsingEvent::class, - static function (PostCollectFilesForParsingEvent $event) use ($parsingProgressBar, &$parsingStartTime): void { - // Each File needs to be first parsed then rendered - $parsingStartTime = microtime(true); - $parsingProgressBar->setMaxSteps(count($event->getFiles())); - }, - ); - $this->eventDispatcher->addListener( - PreParseDocument::class, - static function (PreParseDocument $event) use ($parsingProgressBar): void { - $parsingProgressBar->setMessage('Parsing file: ' . $event->getFileName()); - $parsingProgressBar->display(); - }, - ); - $this->eventDispatcher->addListener( - PostParseDocument::class, - static function (PostParseDocument $event) use ($parsingProgressBar): void { - $parsingProgressBar->advance(); - }, - ); - $this->eventDispatcher->addListener( - PostParseProcess::class, - static function (PostParseProcess $event) use ($parsingProgressBar, $parsingStartTime): void { - $parsingTimeElapsed = microtime(true) - $parsingStartTime; - $parsingProgressBar->setMessage(sprintf( - 'Parsed %s files in %.2f seconds', - $parsingProgressBar->getMaxSteps(), - $parsingTimeElapsed, - )); - $parsingProgressBar->finish(); - }, - ); - $that = $this; - $this->eventDispatcher->addListener( - PreRenderProcess::class, - static function (PreRenderProcess $event) use ($that, $output): void { - $renderingProgressBar = new ProgressBar($output->section(), count($event->getCommand()->getDocumentArray())); - $renderingProgressBar->setFormat('Rendering: %current%/%max% [%bar%] %percent:3s%% Output format ' . $event->getCommand()->getOutputFormat() . ': %message%'); - $renderingStartTime = microtime(true); - $that->eventDispatcher->addListener( - PreRenderDocument::class, - static function (PreRenderDocument $event) use ($renderingProgressBar): void { - $renderingProgressBar->setMessage('Rendering: ' . $event->getCommand()->getFileDestination()); - $renderingProgressBar->display(); - }, - ); - $that->eventDispatcher->addListener( - PostRenderDocument::class, - static function (PostRenderDocument $event) use ($renderingProgressBar): void { - $renderingProgressBar->advance(); - }, - ); - $that->eventDispatcher->addListener( - PostRenderProcess::class, - static function (PostRenderProcess $event) use ($renderingProgressBar, $renderingStartTime): void { - $renderingElapsedTime = microtime(true) - $renderingStartTime; - $renderingProgressBar->setMessage(sprintf( - 'Rendered %s documents in %.2f seconds', - $renderingProgressBar->getMaxSteps(), - $renderingElapsedTime, - )); - $renderingProgressBar->finish(); - }, - ); - }, + Deprecation::trigger( + 'phpdocumentor/guides-cli', + 'https://github.com/phpDocumentor/guides/issues/1210', + 'Progressbar will be registered via settings', ); + $this->progressBarSubscriber->subscribe($output, $this->eventDispatcher); } private function getSettingsOverriddenWithInput(InputInterface $input): ProjectSettings @@ -277,6 +208,16 @@ private function getSettingsOverriddenWithInput(InputInterface $input): ProjectS $settings->setTheme((string) $input->getOption('theme')); } + if (method_exists($settings, 'setExcludes')) { + /** @var list $excludePaths */ + $excludePaths = (array) $input->getOption('exclude-path'); + if ($excludePaths !== []) { + $settings->setExcludes( + $settings->getExcludes()->withPaths($excludePaths), + ); + } + } + return $settings; } @@ -323,32 +264,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($output instanceof ConsoleOutputInterface && $settings->isShowProgressBar()) { - $this->registerProgressBar($output); + $this->progressBarSubscriber->subscribe($output, $this->eventDispatcher); } if ($settings->getInputFile() === '') { - $exclude = null; - if ($input->getOption('exclude-path')) { - /** @var string[] $excludedPaths */ - $excludedPaths = (array) $input->getOption('exclude-path'); - $excludedSpecifications = array_map(static fn (string $path) => new NotSpecification(new InPath(new Path($path))), $excludedPaths); - $excludedSpecification = array_shift($excludedSpecifications); - assert($excludedSpecification !== null); - - $exclude = array_reduce( - $excludedSpecifications, - static fn (SpecificationInterface $carry, SpecificationInterface $spec) => new OrSpecification($carry, $spec), - $excludedSpecification, - ); - } - $documents = $this->commandBus->handle( new ParseDirectoryCommand( $sourceFileSystem, '', $settings->getInputFormat(), $projectNode, - $exclude, + $this->getExclude($settings, $input), ), ); } else { @@ -405,4 +331,31 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::SUCCESS; } + + private function getExclude(ProjectSettings $settings, InputInterface|null $input = null): Exclude|SpecificationInterface|null + { + if (method_exists($settings, 'getExcludes')) { + return $settings->getExcludes(); + } + + if ($input === null) { + return null; + } + + if ($input->getOption('exclude-path')) { + /** @var string[] $excludedPaths */ + $excludedPaths = (array) $input->getOption('exclude-path'); + $excludedSpecifications = array_map(static fn (string $path) => new NotSpecification(new InPath(new Path($path))), $excludedPaths); + $excludedSpecification = array_shift($excludedSpecifications); + assert($excludedSpecification !== null); + + return array_reduce( + $excludedSpecifications, + static fn (SpecificationInterface $carry, SpecificationInterface $spec) => new OrSpecification($carry, $spec), + $excludedSpecification, + ); + } + + return null; + } } diff --git a/packages/guides/composer.json b/packages/guides/composer.json index 9417f1bfc..128d5faa3 100644 --- a/packages/guides/composer.json +++ b/packages/guides/composer.json @@ -29,6 +29,7 @@ "league/tactician": "^1.1", "league/uri": "^7.5.1", "phpdocumentor/flyfinder": "^1.1 || ^2.0", + "phpdocumentor/filesystem": "^1.7.0", "psr/event-dispatcher": "^1.0", "symfony/clock": "^6.4.8", "symfony/html-sanitizer": "^6.4.8", diff --git a/packages/guides/src/DependencyInjection/GuidesExtension.php b/packages/guides/src/DependencyInjection/GuidesExtension.php index 52e61a8b1..fa7a7a9b8 100644 --- a/packages/guides/src/DependencyInjection/GuidesExtension.php +++ b/packages/guides/src/DependencyInjection/GuidesExtension.php @@ -13,6 +13,7 @@ namespace phpDocumentor\Guides\DependencyInjection; +use phpDocumentor\FileSystem\Finder\Exclude; use phpDocumentor\Guides\Compiler\NodeTransformers\RawNodeEscapeTransformer; use phpDocumentor\Guides\DependencyInjection\Compiler\NodeRendererPass; use phpDocumentor\Guides\DependencyInjection\Compiler\ParserRulesPass; @@ -118,6 +119,15 @@ static function ($value) { ->scalarNode('input')->end() ->scalarNode('input_file')->end() ->scalarNode('index_name')->end() + ->arrayNode('exclude') + ->addDefaultsIfNotSet() + ->fixXmlConfig('path') + ->children() + ->booleanNode('hidden')->defaultTrue()->end() + ->booleanNode('symlinks')->defaultTrue()->end() + ->append($this->paths()) + ->end() + ->end() ->scalarNode('output')->end() ->scalarNode('input_format')->end() ->arrayNode('output_format') @@ -317,6 +327,14 @@ public function load(array $configs, ContainerBuilder $container): void $projectSettings->setDefaultCodeLanguage((string) $config['default_code_language']); } + $projectSettings->setExcludes( + new Exclude( + $config['exclude']['paths'], + $config['exclude']['hidden'], + $config['exclude']['symlinks'], + ), + ); + $container->getDefinition(SettingsManager::class) ->addMethodCall('setProjectSettings', [$projectSettings]); @@ -337,6 +355,20 @@ public function load(array $configs, ContainerBuilder $container): void } } + /** @param array $defaultValue */ + private function paths(array $defaultValue = []): ArrayNodeDefinition + { + $treebuilder = new TreeBuilder('paths'); + + return $treebuilder->getRootNode() + ->beforeNormalization() + ->castToArray() + ->end() + ->defaultValue($defaultValue) + ->prototype('scalar') + ->end(); + } + public function process(ContainerBuilder $container): void { (new ParserRulesPass())->process($container); diff --git a/packages/guides/src/FileCollector.php b/packages/guides/src/FileCollector.php index 70610b117..63c1651bf 100644 --- a/packages/guides/src/FileCollector.php +++ b/packages/guides/src/FileCollector.php @@ -13,7 +13,6 @@ namespace phpDocumentor\Guides; -use Flyfinder\Path; use Flyfinder\Specification\AndSpecification; use Flyfinder\Specification\HasExtension; use Flyfinder\Specification\InPath; @@ -22,6 +21,10 @@ use InvalidArgumentException; use League\Flysystem\FilesystemInterface; use phpDocumentor\FileSystem\FileSystem; +use phpDocumentor\FileSystem\Finder\Exclude; +use phpDocumentor\FileSystem\Finder\SpecificationFactory; +use phpDocumentor\FileSystem\Finder\SpecificationFactoryInterface; +use phpDocumentor\FileSystem\Path; use function sprintf; use function strlen; @@ -32,6 +35,12 @@ final class FileCollector { /** @var string[][] */ private array $fileInfos = []; + private SpecificationFactoryInterface $specificationFactory; + + public function __construct(SpecificationFactoryInterface|null $specificationFactory = null) + { + $this->specificationFactory = $specificationFactory ?? new SpecificationFactory(); + } /** * Scans a directory recursively looking for all files to parse. @@ -40,15 +49,12 @@ final class FileCollector * objects, and avoids adding files to the parse queue that have * not changed and whose direct dependencies have not changed. * - * @param SpecificationInterface|null $excludedSpecification specification that is used to exclude specific files/directories + * @param SpecificationInterface|Exclude|null $excludedSpecification specification that is used to exclude specific files/directories */ - public function collect(FilesystemInterface|FileSystem $filesystem, string $directory, string $extension, SpecificationInterface|null $excludedSpecification = null): Files + public function collect(FilesystemInterface|FileSystem $filesystem, string $directory, string $extension, SpecificationInterface|Exclude|null $excludedSpecification = null): Files { $directory = trim($directory, '/'); - $specification = new AndSpecification(new InPath(new Path($directory)), new HasExtension([$extension])); - if ($excludedSpecification) { - $specification = new AndSpecification($specification, new NotSpecification($excludedSpecification)); - } + $specification = $this->getSpecification($excludedSpecification, $directory, $extension); /** @var array> $files */ $files = $filesystem->find($specification); @@ -88,49 +94,7 @@ private function doesFileRequireParsing(string $filename): bool ); } - // TODO: introduce caching again? return true; - -// $file = $this->fileInfos[$filename]; -// $documentFilename = $this->getFilenameFromFile($file); -// $entry = $this->metas->findDocument($documentFilename); -// -// // Look to the file's dependencies to know if you need to parse it or not -// $dependencies = $entry !== null ? $entry->getDepends() : []; -// -// if ($entry !== null && $entry->getParent() !== null) { -// $dependencies[] = $entry->getParent(); -// } -// -// foreach ($dependencies as $dependency) { -// /* -// * The dependency check is NOT recursive on purpose. -// * If fileA has a link to fileB that uses its "headline", -// * for example, then fileA is "dependent" on fileB. If -// * fileB changes, it means that its MetaEntry needs to -// * be updated. And because fileA gets the headline from -// * the MetaEntry, it means that fileA must also be re-parsed. -// * However, if fileB depends on fileC and file C only is -// * updated, fileB *does* need to be re-parsed, but fileA -// * does not, because the MetaEntry for fileB IS still -// * "fresh" - fileB did not actually change, so any metadata -// * about headlines, etc, is still fresh. Therefore, fileA -// * does not need to be parsed. -// */ -// -// // dependency no longer exists? We should re-parse this file -// if (!isset($this->fileInfos[$dependency])) { -// return true; -// } -// -// // finally, we need to recursively ask if this file needs parsing -// if ($this->hasFileBeenUpdated($dependency)) { -// return true; -// } -// } - - // Meta is fresh and no dependencies need parsing - //return false; } /** @@ -142,4 +106,22 @@ private function getFilenameFromFile(string $filename, string $dirname): string return $directory . $filename; } + + private function getSpecification(Exclude|SpecificationInterface|null $excludedSpecification, string $directory, string $extension): SpecificationInterface + { + if ($excludedSpecification instanceof Exclude) { + if ($directory === '') { + $directory = new Path('./'); + } + + return $this->specificationFactory->create([$directory], $excludedSpecification, [$extension]); + } + + $specification = new AndSpecification(new InPath(new \Flyfinder\Path($directory)), new HasExtension([$extension])); + if ($excludedSpecification) { + $specification = new AndSpecification($specification, new NotSpecification($excludedSpecification)); + } + + return $specification; + } } diff --git a/packages/guides/src/Handlers/ParseDirectoryCommand.php b/packages/guides/src/Handlers/ParseDirectoryCommand.php index 369d54ccc..8a40ab423 100644 --- a/packages/guides/src/Handlers/ParseDirectoryCommand.php +++ b/packages/guides/src/Handlers/ParseDirectoryCommand.php @@ -13,20 +13,38 @@ namespace phpDocumentor\Guides\Handlers; +use Doctrine\Deprecations\Deprecation; use Flyfinder\Specification\SpecificationInterface; use League\Flysystem\FilesystemInterface; use phpDocumentor\FileSystem\FileSystem; +use phpDocumentor\FileSystem\Finder\Exclude; use phpDocumentor\Guides\Nodes\ProjectNode; final class ParseDirectoryCommand { + private readonly SpecificationInterface|null $excludedSpecification; + private readonly Exclude|null $exclude; + public function __construct( private readonly FilesystemInterface|FileSystem $origin, private readonly string $directory, private readonly string $inputFormat, private readonly ProjectNode $projectNode, - private readonly SpecificationInterface|null $excludedSpecification = null, + SpecificationInterface|Exclude|null $excludedSpecification = null, ) { + if ($excludedSpecification instanceof SpecificationInterface) { + Deprecation::trigger( + 'phpDocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues/1209', + 'Passing ' . $excludedSpecification::class . ' to ' . self::class . 'will be deprecated,' + . 'use phpDocumentor\FileSystem\Finder\Exclude instead.', + ); + $this->excludedSpecification = $excludedSpecification; + $this->exclude = null; + } else { + $this->exclude = $excludedSpecification; + $this->excludedSpecification = null; + } } public function getOrigin(): FilesystemInterface|FileSystem @@ -49,8 +67,25 @@ public function getProjectNode(): ProjectNode return $this->projectNode; } + /** @deprecated Specification definition on parse directory is deprecated. Use @{see self::getExclude()} instead.*/ public function getExcludedSpecification(): SpecificationInterface|null { - return $this->excludedSpecification; + Deprecation::trigger( + 'phpDocumentor/guides', + 'https://github.com/phpDocumentor/guides/issues/1209', + 'Specification definition on parse directory is deprecated. Use getExclude() instead.', + ); + + return $this->excludedSpecification ?? null; + } + + public function getExclude(): Exclude + { + return $this->exclude ?? new Exclude(); + } + + public function hasExclude(): bool + { + return isset($this->exclude); } } diff --git a/packages/guides/src/Handlers/ParseDirectoryHandler.php b/packages/guides/src/Handlers/ParseDirectoryHandler.php index 870943191..f77435f44 100644 --- a/packages/guides/src/Handlers/ParseDirectoryHandler.php +++ b/packages/guides/src/Handlers/ParseDirectoryHandler.php @@ -65,7 +65,12 @@ public function handle(ParseDirectoryCommand $command): array $extension, ); - $files = $this->fileCollector->collect($origin, $currentDirectory, $extension, $command->getExcludedSpecification()); + $files = $this->fileCollector->collect( + $origin, + $currentDirectory, + $extension, + $command->hasExclude() ? $command->getExclude() : $command->getExcludedSpecification(), + ); $postCollectFilesForParsingEvent = $this->eventDispatcher->dispatch( new PostCollectFilesForParsingEvent($command, $files), diff --git a/packages/guides/src/Settings/ProjectSettings.php b/packages/guides/src/Settings/ProjectSettings.php index 08bdfd3eb..2c2bc28a0 100644 --- a/packages/guides/src/Settings/ProjectSettings.php +++ b/packages/guides/src/Settings/ProjectSettings.php @@ -13,6 +13,8 @@ namespace phpDocumentor\Guides\Settings; +use phpDocumentor\FileSystem\Finder\Exclude; + final class ProjectSettings { /** @var array */ @@ -39,6 +41,12 @@ final class ProjectSettings /** @var string[] */ private array $ignoredDomains = []; + private Exclude $excludes; + + public function __construct() + { + $this->excludes = new Exclude(); + } public function getTitle(): string { @@ -256,4 +264,14 @@ public function setAutomaticMenu(bool $automaticMenu): ProjectSettings return $this; } + + public function setExcludes(Exclude $exclude): void + { + $this->excludes = $exclude; + } + + public function getExcludes(): Exclude + { + return $this->excludes; + } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a9c069328..d897bc64f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,6 +5,16 @@ parameters: count: 2 path: packages/guides-cli/src/Command/Run.php + - + message: "#^Call to function method_exists\\(\\) with phpDocumentor\\\\Guides\\\\Settings\\\\ProjectSettings and 'getExcludes' will always evaluate to true\\.$#" + count: 1 + path: packages/guides-cli/src/Command/Run.php + + - + message: "#^Call to function method_exists\\(\\) with phpDocumentor\\\\Guides\\\\Settings\\\\ProjectSettings and 'setExcludes' will always evaluate to true\\.$#" + count: 1 + path: packages/guides-cli/src/Command/Run.php + - message: "#^Cannot cast mixed to string\\.$#" count: 6 @@ -140,6 +150,11 @@ parameters: count: 1 path: packages/guides/src/DependencyInjection/Compiler/RendererPass.php + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:prototype\\(\\)\\.$#" + count: 1 + path: packages/guides/src/DependencyInjection/GuidesExtension.php + - message: "#^Cannot call method scalarNode\\(\\) on Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\|null\\.$#" count: 1 diff --git a/tests/Integration/tests/configuration/config-exclude/expected/index.html b/tests/Integration/tests/configuration/config-exclude/expected/index.html new file mode 100644 index 000000000..82ad60194 --- /dev/null +++ b/tests/Integration/tests/configuration/config-exclude/expected/index.html @@ -0,0 +1,10 @@ + +
+

Document Title

+ +

Lorem Ipsum Dolor.

+ +
$some = 'PHP code';
+ +
+ diff --git a/tests/Integration/tests/configuration/config-exclude/input/guides.xml b/tests/Integration/tests/configuration/config-exclude/input/guides.xml new file mode 100644 index 000000000..4d15f300e --- /dev/null +++ b/tests/Integration/tests/configuration/config-exclude/input/guides.xml @@ -0,0 +1,10 @@ + + + + ignore.rst + + diff --git a/tests/Integration/tests/configuration/config-exclude/input/ignore.rst b/tests/Integration/tests/configuration/config-exclude/input/ignore.rst new file mode 100644 index 000000000..b5a77161e --- /dev/null +++ b/tests/Integration/tests/configuration/config-exclude/input/ignore.rst @@ -0,0 +1,3 @@ +============== +Ignored +============== diff --git a/tests/Integration/tests/configuration/config-exclude/input/index.rst b/tests/Integration/tests/configuration/config-exclude/input/index.rst new file mode 100644 index 000000000..84a11ed31 --- /dev/null +++ b/tests/Integration/tests/configuration/config-exclude/input/index.rst @@ -0,0 +1,9 @@ +============== +Document Title +============== + +Lorem Ipsum Dolor. + +:: + + $some = 'PHP code';