diff --git a/composer.json b/composer.json index 96cdc3f..781043a 100644 --- a/composer.json +++ b/composer.json @@ -46,6 +46,9 @@ "main": "2.x-dev" } }, + "bin": [ + "depanno" + ], "archive": { "exclude": [ ".github", diff --git a/depanno b/depanno new file mode 100644 index 0000000..9bcb689 --- /dev/null +++ b/depanno @@ -0,0 +1,45 @@ +#!/usr/bin/env php +')) { + fwrite( + STDERR, + "This version of DepAnno requires PHP 7.1.0 or greater." . PHP_EOL . + "You are currently using PHP " . PHP_VERSION . PHP_EOL + ); + die(1); +} + +if (!ini_get('date.timezone')) { + ini_set('date.timezone', 'UTC'); +} + +$found = false; +foreach ( + [ + __DIR__ . '/../../autoload.php', + __DIR__ . '/../vendor/autoload.php', + __DIR__ . '/vendor/autoload.php', + ] as $autoload +) { + if (file_exists($autoload)) { + require_once($autoload); + $found = true; + break; + } +} + +if (!$found) { + fwrite(STDERR, "You must install DepAnno using Composer"); + die(1); +} + +die(CliApplication::execute()); diff --git a/src/Controller/CliApplication.php b/src/Controller/CliApplication.php new file mode 100644 index 0000000..4a3a73b --- /dev/null +++ b/src/Controller/CliApplication.php @@ -0,0 +1,25 @@ +add(new WhyBlockCommandController()); + return $application->run(); + } +} diff --git a/src/Controller/WhyBlockCommandController.php b/src/Controller/WhyBlockCommandController.php new file mode 100644 index 0000000..4421c9e --- /dev/null +++ b/src/Controller/WhyBlockCommandController.php @@ -0,0 +1,158 @@ + CsvOutputHandler::class, + self::FORMAT_TEXT => StandardOutputHandler::class, + self::FORMAT_JSON => JsonOutputHandler::class, + self::FORMAT_XML => XmlOutputHandler::class, + ]; + + // phpcs:disable Generic.Files.LineLength.TooLong -- Attribute support pre PHP 8 + #[Dependency('symfony/console', '^5', 'Command\'s setName, addArgument and addOption methods as well as InputArgument\'s constants of REQUIRED and VALUE_NONE')] + #[Dependency('php-di/php-di', '^6', 'DI\ContainerBuilder::addDefinitions and the existence of the DI\autowire function')] + // phpcs:enable Generic.Files.LineLength.TooLong + protected function configure(): void + { + $this->setName('why-block') + ->addArgument('package', InputArgument::REQUIRED, 'Package to inspect') + ->addArgument('version', InputArgument::REQUIRED, 'Version you want to update it to') + ->addArgument('directory', InputArgument::REQUIRED, 'Directory to search in') + ->addOption( + self::OUTPUT_FORMAT, + ['f'], + InputOption::VALUE_OPTIONAL, + 'Format to output results in. Accepted values: text, csv, json, xml', + 'text' + ) + ->addOption( + self::FAIL_ON_ERROR, + ['e'], + InputOption::VALUE_NONE, + 'Immediately fail on parsing errors' + ) + ->addOption( + self::LEGACY_ANNOTATION, + ['l'], + InputOption::VALUE_NONE, + 'Include old @dependency/@composerDependency annotations in search' + ); + } + + #[Dependency('symfony/console', '^5', 'InputInterface::getOption and OutputInterface::writeln')] + protected function execute( + InputInterface $input, + OutputInterface $output + ): int { + $packageToSearchFor = $input->getArgument('package'); + $versionToCompareTo = $input->getArgument('version'); + $directory = $input->getArgument('directory'); + $outputFormat = $input->getOption(self::OUTPUT_FORMAT); + + if (!is_string($directory)) { + throw new InvalidArgumentException('Only one directory is allowed'); + } + if (!is_string($packageToSearchFor)) { + throw new InvalidArgumentException('Only one package is allowed'); + } + if (!is_string($versionToCompareTo)) { + throw new InvalidArgumentException('Only one version is allowed'); + } + if (!is_string($outputFormat)) { + throw new InvalidArgumentException('Only one output format is allowed'); + } + + $outputFormat = strtolower($outputFormat); + if (!in_array($outputFormat, static::ACCEPTABLE_FORMATS)) { + $outputFormat = 'text'; + } + + $containerBuilder = new ContainerBuilder(); + $containerBuilder->addDefinitions( + [ + InputInterface::class => $input, + OutputInterface::class => $output, + IssueHandlerInterface::class => $input->getOption(static::FAIL_ON_ERROR) + ? FailOnIssueHandler::class + : NotifyOnIssueHandler::class, + ParserInterface::class => static function (ContainerInterface $container) use ($input) { + $parsers = [$container->get(AstParser::class)]; + if ($input->getOption(static::LEGACY_ANNOTATION)) { + $parsers[] = $container->get(LegacyParser::class); + } + return new ParserPool($parsers); + }, + WriterInterface::class => autowire(StdOutWriter::class), + ScopeDeterminerInterface::class => static function (ContainerInterface $container) use ($directory) { + return new DirectoryScopeDeterminer( + $container->get(PhpFileFinder::class), + $directory + ); + }, + OutputHandlerInterface::class => autowire(static::FORMAT_MAPPER[$outputFormat]), + ] + ); + $container = $containerBuilder->build(); + + /** @var WhyBlockCommand $command */ + $command = $container->get(WhyBlockCommand::class); + return $command->execute($packageToSearchFor, $versionToCompareTo); + } +} diff --git a/src/ScopeDeterminer/ComposerScopeDeterminer.php b/src/ScopeDeterminer/ComposerScopeDeterminer.php index 7af77d9..507b2f3 100644 --- a/src/ScopeDeterminer/ComposerScopeDeterminer.php +++ b/src/ScopeDeterminer/ComposerScopeDeterminer.php @@ -26,20 +26,20 @@ class ComposerScopeDeterminer implements ScopeDeterminerInterface /** @var Composer */ private $composer; - /** @var PhpFileDeterminer */ - private $phpFileDeterminer; + /** @var PhpFileFinder */ + private $phpFileFinder; /** @var int */ private $scope; public function __construct( Composer $composer, - PhpFileDeterminer $phpFileDeterminer, + PhpFileFinder $phpFileFinder, #[ExpectedValues(valuesFromClass: ComposerScopeDeterminer::class)] int $scope = self::SCOPE_PROJECT_ONLY ) { $this->composer = $composer; - $this->phpFileDeterminer = $phpFileDeterminer; + $this->phpFileFinder = $phpFileFinder; $this->scope = $scope; } @@ -133,40 +133,10 @@ private function getAllFilesForAutoload( continue; } if (is_dir($realDir)) { - $results = $this->getAllPhpFiles($realDir, $results); + $results = $this->phpFileFinder->findAll($dir, $results); } } } return $results; } - - /** - * Find all PHP files by recursively searching a directory - * - * @param string $dir Directory to search recursively - * @param string[] $results Array of file paths to merge with - * @return string[] File paths - */ - private function getAllPhpFiles(string $dir, array $results = []): array - { - $files = scandir($dir); - if ($files === false) { - return $results; - } - - foreach ($files as $value) { - $path = realpath($dir . DIRECTORY_SEPARATOR . $value); - if ($path === false) { - continue; - } - - if (is_file($path) && $this->phpFileDeterminer->isPhp($path)) { - $results[] = $path; - } elseif (is_dir($path) && !in_array($value, ['.', '..'])) { - $results = $this->getAllPhpFiles($path, $results); - } - } - - return $results; - } } diff --git a/src/ScopeDeterminer/DirectoryScopeDeterminer.php b/src/ScopeDeterminer/DirectoryScopeDeterminer.php new file mode 100644 index 0000000..faabe68 --- /dev/null +++ b/src/ScopeDeterminer/DirectoryScopeDeterminer.php @@ -0,0 +1,29 @@ +directory = $directory; + $this->phpfileFinder = $phpfileFinder; + } + + public function getFiles(): array + { + return $this->phpfileFinder->findAll($this->directory); + } +} diff --git a/src/ScopeDeterminer/PhpFileFinder.php b/src/ScopeDeterminer/PhpFileFinder.php new file mode 100644 index 0000000..9aca361 --- /dev/null +++ b/src/ScopeDeterminer/PhpFileFinder.php @@ -0,0 +1,53 @@ +phpFileDeterminer = $phpFileDeterminer; + } + + /** + * Find all PHP files by recursively searching a directory + * + * @param string $dir Directory to search recursively + * @param string[] $results Array of file paths to merge with + * @return string[] File paths + */ + public function findAll(string $dir, array $results = []): array + { + // Directories is ever expanding by the loop. We do this instead of recursion b/c I have an unhealthy fear + // of recursion limits + $directories = [$dir]; + for ($i = 0; $i < count($directories); ++$i) { + $files = scandir($directories[$i]); + if ($files !== false) { + foreach ($files as $value) { + $path = realpath($directories[$i] . DIRECTORY_SEPARATOR . $value); + if ($path === false) { + continue; + } + + if (is_file($path) && $this->phpFileDeterminer->isPhp($path)) { + $results[] = $path; + } elseif (is_dir($path) && !in_array($value, ['.', '..'])) { + $directories[] = $path; + } + } + } + } + + return $results; + } +} diff --git a/tests/ScopeDeterminer/DirectoryScopeDeterminerTest.php b/tests/ScopeDeterminer/DirectoryScopeDeterminerTest.php new file mode 100644 index 0000000..f618b5a --- /dev/null +++ b/tests/ScopeDeterminer/DirectoryScopeDeterminerTest.php @@ -0,0 +1,44 @@ +createMock(PhpFileFinder::class); + $finder->expects($this->once()) + ->method('findAll') + ->with($dir) + ->willReturn([]); + + $determiner = new DirectoryScopeDeterminer($finder, $dir); + $determiner->getFiles(); + } + + public function testResultOfPhpFileFinderIsProvidedBack(): void + { + $results = [ + uniqid(), + uniqid() + ]; + + $finder = $this->createMock(PhpFileFinder::class); + $finder->method('findAll') + ->willReturn($results); + + $determiner = new DirectoryScopeDeterminer($finder, uniqid()); + $this->assertEquals($results, $determiner->getFiles()); + } +} diff --git a/tests/ScopeDeterminer/PhpFileFinderTest.php b/tests/ScopeDeterminer/PhpFileFinderTest.php new file mode 100644 index 0000000..8042e4b --- /dev/null +++ b/tests/ScopeDeterminer/PhpFileFinderTest.php @@ -0,0 +1,51 @@ +get(PhpFileFinder::class); + $results = $finder->findAll($directory); + + $this->assertIsArray($results); + $this->assertCount(5, $results); + + foreach ($files as $file) { + $this->assertContains($file, $results); + } + } +} diff --git a/tests/_data/phpFileFinder/file1.php b/tests/_data/phpFileFinder/file1.php new file mode 100644 index 0000000..8767608 --- /dev/null +++ b/tests/_data/phpFileFinder/file1.php @@ -0,0 +1,3 @@ +