diff --git a/phpstan.neon b/phpstan.neon index 1ff472c..689096b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,13 @@ parameters: level: 8 paths: - src + ignoreErrors: + - message: "#Cannot cast array\\\\|string\\|null to string\\.#" + path: src/Controller/Cli/WhyBlockCliCommand.php + count: 1 + - message: "#Cannot cast array\\\\|string\\|null to string\\.#" + path: src/Controller/Common/WhyBlockUtility/OptionValuesBuilder.php + count: 2 + - message: "#Cannot cast array\\\\|bool\\|string\\|null to string\\.#" + path: src/Controller/Common/WhyBlockUtility/OptionValuesBuilder.php + count: 1 diff --git a/src/Controller/Cli/WhyBlockCliCommand.php b/src/Controller/Cli/WhyBlockCliCommand.php new file mode 100644 index 0000000..89e08f6 --- /dev/null +++ b/src/Controller/Cli/WhyBlockCliCommand.php @@ -0,0 +1,69 @@ +addArgument('directory', InputArgument::REQUIRED, 'Directory to search in'); + } + + #[Dependency('symfony/console', '^5', 'InputInterface::getOption and OutputInterface::writeln')] + protected function execute( + InputInterface $input, + OutputInterface $output + ): int { + $optionValuesBuilder = new WhyBlockUtility\OptionValuesBuilder(); + $optionValues = $optionValuesBuilder->buildFromInput($input); + $directory = (string)$input->getArgument('directory'); + + $containerBuilder = AddDefaultDefinitions::execute(); + WhyBlockUtility::addBaseDiDefinitions($containerBuilder, $input, $output, $optionValues); + $containerBuilder->addDefinitions( + [ + ScopeDeterminerInterface::class => static function (ContainerInterface $container) use ($directory) { + return new DirectoryScopeDeterminer( + $container->get(PhpFileFinder::class), + $directory + ); + }, + ] + ); + $container = $containerBuilder->build(); + + /** @var WhyBlockCommand $command */ + $command = $container->get(WhyBlockCommand::class); + return $command->execute( + $optionValues->getPackageToSearchFor(), + $optionValues->getVersionToCompareTo() + ); + } +} diff --git a/src/Controller/CliApplication.php b/src/Controller/CliApplication.php index 4a3a73b..c6e49de 100644 --- a/src/Controller/CliApplication.php +++ b/src/Controller/CliApplication.php @@ -9,6 +9,7 @@ namespace Navarr\Depends\Controller; use Navarr\Attribute\Dependency; +use Navarr\Depends\Controller\Cli\WhyBlockCliCommand; use Symfony\Component\Console\Application; #[Dependency('symfony/console', '^5', 'Creates a Symfony Application')] @@ -19,7 +20,7 @@ class CliApplication public static function execute(): int { $application = new Application('DepAnno', static::VERSION); - $application->add(new WhyBlockCommandController()); + $application->add(new WhyBlockCliCommand()); return $application->run(); } } diff --git a/src/Controller/Common/WhyBlockUtility.php b/src/Controller/Common/WhyBlockUtility.php new file mode 100644 index 0000000..8f3073d --- /dev/null +++ b/src/Controller/Common/WhyBlockUtility.php @@ -0,0 +1,126 @@ + CsvOutputHandler::class, + self::FORMAT_TEXT => StandardOutputHandler::class, + self::FORMAT_JSON => JsonOutputHandler::class, + self::FORMAT_XML => XmlOutputHandler::class, + ]; + + // phpcs:ignore 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')] + public static function addCommongArguments( + Command $command + ): Command { + return $command->setName('why-block') + ->addArgument( + static::ARGUMENT_PACKAGE, + InputArgument::REQUIRED, + 'Package to search dependency attributes for' + ) + ->addArgument( + static::ARGUMENT_VERSION, + InputArgument::REQUIRED, + 'Version you want to update the package to' + ) + ->addOption( + static::OPTION_OUTPUT_FORMAT, + ['f'], + InputOption::VALUE_OPTIONAL, + 'Format to put results in. Accepted values: text, csv, json, xml', + 'text' + ) + ->addOption( + static::OPTION_FAIL_ON_ERROR, + ['e'], + InputOption::VALUE_NONE, + 'Immediately fail on parsing errors' + ) + ->addOption( + static::OPTION_INCLUDE_ANNOTATIONS, + ['l'], + InputOption::VALUE_NONE, + 'Include old @dependency/@composerDependency annotations in search' + ); + } + + public static function addBaseDiDefinitions( + ContainerBuilder $containerBuilder, + InputInterface $input, + OutputInterface $output, + OptionValues $optionValues + ): ContainerBuilder { + $formatMapper = WhyBlockUtility::FORMAT_MAPPER[$optionValues->getOutputFormat()]; + $containerBuilder->addDefinitions( + [ + InputInterface::class => $input, + OutputInterface::class => $output, + IssueHandlerInterface::class => $optionValues->shouldFailOnParseError() + ? FailOnIssueHandler::class + : NotifyOnIssueHandler::class, + ParserInterface::class => static function (ContainerInterface $container) use ($optionValues) { + $parsers = [$container->get(AstParser::class)]; + if ($optionValues->shouldIncludeAnnotationsInSearch()) { + $parsers[] = $container->get(LegacyParser::class); + } + return new ParserPool($parsers); + }, + OutputHandlerInterface::class => autowire($formatMapper), + ] + ); + return $containerBuilder; + } +} diff --git a/src/Controller/Common/WhyBlockUtility/OptionValues.php b/src/Controller/Common/WhyBlockUtility/OptionValues.php new file mode 100644 index 0000000..de054c6 --- /dev/null +++ b/src/Controller/Common/WhyBlockUtility/OptionValues.php @@ -0,0 +1,69 @@ +package = $package; + $this->version = $version; + $this->format = $format; + $this->includeLegacy = $includeLegacy; + $this->failOnError = $failOnError; + } + + public function getPackageToSearchFor(): string + { + return $this->package; + } + + public function getVersionToCompareTo(): string + { + return $this->version; + } + + public function getOutputFormat(): string + { + return $this->format; + } + + public function shouldIncludeAnnotationsInSearch(): bool + { + return $this->includeLegacy; + } + + public function shouldFailOnParseError(): bool + { + return $this->failOnError; + } +} diff --git a/src/Controller/Common/WhyBlockUtility/OptionValuesBuilder.php b/src/Controller/Common/WhyBlockUtility/OptionValuesBuilder.php new file mode 100644 index 0000000..3bd492d --- /dev/null +++ b/src/Controller/Common/WhyBlockUtility/OptionValuesBuilder.php @@ -0,0 +1,37 @@ +getArgument(WhyBlockUtility::ARGUMENT_PACKAGE); + $versionToCompareTo = (string)$input->getArgument(WhyBlockUtility::ARGUMENT_VERSION); + $outputFormat = (string)$input->getOption(WhyBlockUtility::OPTION_OUTPUT_FORMAT); + + if (!in_array($outputFormat, WhyBlockUtility::ACCEPTABLE_FORMATS)) { + $outputFormat = 'text'; + } + + $failOnError = (bool)$input->getOption(WhyBlockUtility::OPTION_FAIL_ON_ERROR); + $includeAnnotations = (bool)$input->getOption(WhyBlockUtility::OPTION_INCLUDE_ANNOTATIONS); + + return new OptionValues( + $packageToSearchFor, + $versionToCompareTo, + $outputFormat, + $includeAnnotations, + $failOnError + ); + } +} diff --git a/src/Controller/Composer/ComposerCommand.php b/src/Controller/Composer/ComposerCommand.php deleted file mode 100644 index 4c2bea1..0000000 --- a/src/Controller/Composer/ComposerCommand.php +++ /dev/null @@ -1,172 +0,0 @@ - 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') - ->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' - ) - ->addOption( - self::ROOT_DEPS, - ['r'], - InputOption::VALUE_NONE, - 'Search root dependencies for the @dependency annotation' - ) - ->addOption( - self::ALL_DEPS, - ['a'], - InputOption::VALUE_NONE, - 'Search all dependencies for the @dependency annotation' - ); - } - - #[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'); - $outputFormat = $input->getOption(self::OUTPUT_FORMAT); - - 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'; - } - - if ($input->getOption(static::ALL_DEPS)) { - $composerScope = ComposerScopeDeterminer::SCOPE_ALL_DEPENDENCIES; - } elseif ($input->getOption(static::ROOT_DEPS)) { - $composerScope = ComposerScopeDeterminer::SCOPE_ROOT_DEPENDENCIES; - } else { - $composerScope = ComposerScopeDeterminer::SCOPE_PROJECT_ONLY; - } - - $containerBuilder = new ContainerBuilder(); - $containerBuilder->addDefinitions( - [ - InputInterface::class => $input, - OutputInterface::class => $output, - IssueHandlerInterface::class => $input->getOption(static::FAIL_ON_ERROR) - ? FailOnIssueHandler::class - : NotifyOnIssueHandler::class, - Composer::class => $this->getComposer(true), - 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), - ComposerScopeDeterminer::class => autowire(ComposerScopeDeterminer::class) - ->property('scope', $composerScope), - ScopeDeterminerInterface::class => autowire(ComposerScopeDeterminer::class), - 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/Controller/Composer/ComposerPlugin.php b/src/Controller/Composer/ComposerPlugin.php index 6c687ff..8c7b243 100644 --- a/src/Controller/Composer/ComposerPlugin.php +++ b/src/Controller/Composer/ComposerPlugin.php @@ -14,8 +14,6 @@ use Composer\Plugin\Capable; use Composer\Plugin\PluginInterface; use Navarr\Attribute\Dependency; -use Navarr\Depends\Command\WhyBlockCommand; -use Symfony\Component\Console\Command\Command; #[Dependency('composer-plugin-api', '^1|^2', 'Reliant Interfaces')] #[Dependency('composer/composer', '^1|^2', 'Existence of IOInterface and Composer class')] @@ -41,7 +39,7 @@ public function getCapabilities(): array public function getCommands(): array { return [ - new ComposerCommand(), + new WhyBlockComposerCommand(), ]; } diff --git a/src/Controller/Composer/WhyBlockComposerCommand.php b/src/Controller/Composer/WhyBlockComposerCommand.php new file mode 100644 index 0000000..a6ac2cc --- /dev/null +++ b/src/Controller/Composer/WhyBlockComposerCommand.php @@ -0,0 +1,90 @@ +addOption( + self::OPTION_ROOT_DEPS, + ['r'], + InputOption::VALUE_NONE, + 'Search root dependencies for the @dependency annotation' + ) + ->addOption( + self::OPTION_ALL_DEPS, + ['a'], + InputOption::VALUE_NONE, + 'Search all dependencies for the @dependency annotation' + ); + } + + // phpcs:disable Generic.Files.LineLength.TooLong -- Attribute support pre PHP 8 + #[Dependency('symfony/console', '^5', 'InputInterface::getOption and OutputInterface::writeln')] + #[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 execute( + InputInterface $input, + OutputInterface $output + ): int { + $optionValuesBuilder = new WhyBlockUtility\OptionValuesBuilder(); + $optionValues = $optionValuesBuilder->buildFromInput($input); + + if ($input->getOption(static::OPTION_ALL_DEPS)) { + $composerScope = ComposerScopeDeterminer::SCOPE_ALL_DEPENDENCIES; + } elseif ($input->getOption(static::OPTION_ROOT_DEPS)) { + $composerScope = ComposerScopeDeterminer::SCOPE_ROOT_DEPENDENCIES; + } else { + $composerScope = ComposerScopeDeterminer::SCOPE_PROJECT_ONLY; + } + + $containerBuilder = AddDefaultDefinitions::execute(); + WhyBlockUtility::addBaseDiDefinitions($containerBuilder, $input, $output, $optionValues); + $containerBuilder->addDefinitions( + [ + Composer::class => $this->getComposer(true), + ComposerScopeDeterminer::class => autowire(ComposerScopeDeterminer::class) + ->property('scope', $composerScope), + ScopeDeterminerInterface::class => autowire(ComposerScopeDeterminer::class), + ] + ); + $container = $containerBuilder->build(); + + /** @var WhyBlockCommand $command */ + $command = $container->get(WhyBlockCommand::class); + return $command->execute( + $optionValues->getPackageToSearchFor(), + $optionValues->getVersionToCompareTo() + ); + } +} diff --git a/src/Controller/Di/AddDefaultDefinitions.php b/src/Controller/Di/AddDefaultDefinitions.php new file mode 100644 index 0000000..683a0f2 --- /dev/null +++ b/src/Controller/Di/AddDefaultDefinitions.php @@ -0,0 +1,29 @@ +addDefinitions( + [ + WriterInterface::class => autowire(StdOutWriter::class), + ] + ); + return $containerBuilder; + } +} diff --git a/src/Controller/WhyBlockCommandController.php b/src/Controller/WhyBlockCommandController.php deleted file mode 100644 index 4421c9e..0000000 --- a/src/Controller/WhyBlockCommandController.php +++ /dev/null @@ -1,158 +0,0 @@ - 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/PhpFileFinder.php b/src/ScopeDeterminer/PhpFileFinder.php index 9aca361..dc57249 100644 --- a/src/ScopeDeterminer/PhpFileFinder.php +++ b/src/ScopeDeterminer/PhpFileFinder.php @@ -27,6 +27,14 @@ public function __construct(PhpFileDeterminer $phpFileDeterminer) */ public function findAll(string $dir, array $results = []): array { + if (is_file($dir) && $this->phpFileDeterminer->isPhp($dir)) { + return [$dir]; + } + + if (!is_dir($dir)) { + return []; + } + // 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]; diff --git a/tests/Controller/AbstractWhyBlockCommandTest.php b/tests/Controller/AbstractWhyBlockCommandTest.php new file mode 100644 index 0000000..358e734 --- /dev/null +++ b/tests/Controller/AbstractWhyBlockCommandTest.php @@ -0,0 +1,42 @@ +createCommand(); + $definition = $command->getDefinition(); + + $this->assertTrue($definition->hasArgument('package')); + $this->assertTrue($definition->hasArgument('version')); + + $this->assertTrue($definition->hasOption('format')); + $this->assertTrue($definition->hasShortcut('f')); + + $this->assertTrue($definition->hasOption('fail-on-error')); + $this->assertTrue($definition->hasShortcut('e')); + + $this->assertTrue($definition->hasOption('include-legacy-annotations')); + $this->assertTrue($definition->hasShortcut('l')); + + return $command; + } +} diff --git a/tests/Controller/Cli/WhyBlockCliCommandTest.php b/tests/Controller/Cli/WhyBlockCliCommandTest.php new file mode 100644 index 0000000..40c29dd --- /dev/null +++ b/tests/Controller/Cli/WhyBlockCliCommandTest.php @@ -0,0 +1,37 @@ +createCommand(); + + $path = realpath(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', '_data', 'emptyFile.php'])); + + $input = new ArgvInput( + ['', 'project', 'constraint', $path], + $command->getDefinition() + ); + $output = $this->createMock(OutputInterface::class); + + $result = $command->run($input, $output); + $this->assertEquals(0, $result); + } +} diff --git a/tests/Controller/Composer/ComposerPluginTest.php b/tests/Controller/Composer/ComposerPluginTest.php index 24d780b..4d2926f 100644 --- a/tests/Controller/Composer/ComposerPluginTest.php +++ b/tests/Controller/Composer/ComposerPluginTest.php @@ -6,7 +6,7 @@ namespace Navarr\Depends\Test\Controller\Composer; use Composer\Plugin\Capability\CommandProvider; -use Navarr\Depends\Controller\Composer\ComposerCommand; +use Navarr\Depends\Controller\Composer\WhyBlockComposerCommand; use Navarr\Depends\Controller\Composer\ComposerPlugin; use PHPUnit\Framework\TestCase; @@ -29,6 +29,6 @@ public function testContainsOnlyComposerCommands() $this->assertIsArray($commands); $this->assertCount(1, $commands); $command = end($commands); - $this->assertInstanceOf(ComposerCommand::class, $command); + $this->assertInstanceOf(WhyBlockComposerCommand::class, $command); } } diff --git a/tests/Controller/Composer/WhyBlockComposerCommandTest.php b/tests/Controller/Composer/WhyBlockComposerCommandTest.php new file mode 100644 index 0000000..7dd492f --- /dev/null +++ b/tests/Controller/Composer/WhyBlockComposerCommandTest.php @@ -0,0 +1,41 @@ +markTestSkipped('Not sure how to properly inject composer'); + + $command = $this->createCommand(); + $composer = new Composer(); + $command->setComposer($composer); + + $input = new ArgvInput( + ['', 'project', 'constraint'], + $command->getDefinition() + ); + $output = $this->createMock(OutputInterface::class); + + $result = $command->run($input, $output); + $this->assertEquals(0, $result); + } +}