Skip to content
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
6 changes: 6 additions & 0 deletions .changelog/explicit-tool-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: fixed
type: patch
---

Honor explicit Rector configuration and forward PHPDoc file/directory selections to both underlying tools. Preserve configured scope when no paths are given and reject missing explicit PHPDoc paths before writing or running fixers.
8 changes: 6 additions & 2 deletions docs/commands/phpdoc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ Usage
Arguments
---------

``path`` (optional)
Path to the file or directory to check. Default: ``.``.
``path...`` (optional)
Files or directories to check. Quote paths containing spaces. Without paths,
each tool uses its configured source paths. Explicit paths are passed to both
tools; PHP-CS-Fixer intersects them with its configured Finder, preserving
exclusions. Rector receives the paths as positional arguments and retains its
configured skips. Missing paths fail before the header or tools are invoked.

Options
-------
Expand Down
8 changes: 5 additions & 3 deletions docs/commands/refactor.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ Options
``--fix, -f``
Automatically fix code refactoring issues. Without this option, runs in dry-run mode.

``--config, -c`` (optional)
Path to the Rector configuration file. Default: ``rector.php``.
``--config=FILE, -c FILE``
Path to the Rector configuration file. Default: ``rector.php``. When supplied,
a value is required; an invalid path fails instead of using the default.

``--progress``
Enable progress output from Rector.
Expand Down Expand Up @@ -70,7 +71,8 @@ Exit Codes
Behavior
---------

- Local ``rector.php`` is preferred when present.
- Explicit ``--config`` is resolved and passed unchanged to Rector. Without it,
local ``rector.php`` is preferred when present.
- Packaged default includes Fast Forward custom Rector rules plus shared Rector sets.
- Progress output is disabled by default; use ``--progress`` to re-enable it in
text mode.
Expand Down
34 changes: 31 additions & 3 deletions src/Console/Command/PhpDocCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
Expand Down Expand Up @@ -114,9 +115,9 @@ protected function configure(): void
)
->addArgument(
name: 'path',
mode: InputOption::VALUE_OPTIONAL,
description: 'Path to the file or directory to check.',
default: ['.'],
mode: InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
description: 'Files or directories to check; omit to use configured paths.',
default: [],
)
->addOption(
name: 'progress',
Expand Down Expand Up @@ -152,6 +153,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$this->log('Checking and fixing PHPDocs...', $input);

$paths = (array) $input->getArgument('path');

foreach ($paths as $path) {
if ('' === $path || ! $this->filesystem->exists($path)) {
return $this->failure('PHPDoc path not found: {path}', $input, [
'path' => $path,
'output' => $processOutput,
]);
}
}

$this->ensureDocHeaderExists($input);

$processBuilder = $this->processBuilder
Expand Down Expand Up @@ -182,6 +194,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$processBuilder = $processBuilder->withArgument('--dry-run');
}

if ([] !== $paths) {
$processBuilder = $processBuilder->withArgument('--path-mode=intersection')->withArgument('--');

foreach ($paths as $path) {
$processBuilder = $processBuilder->withArgument($path);
}
}

$phpCsFixer = $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('php-cs-fixer'), 'fix']);

$processBuilder = $this->processBuilder
Expand All @@ -203,6 +223,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$processBuilder = $processBuilder->withArgument('--dry-run');
}

if ([] !== $paths) {
$processBuilder = $processBuilder->withArgument('--');

foreach ($paths as $path) {
$processBuilder = $processBuilder->withArgument($path);
}
}

$rector = $processBuilder->build([DevToolsPathResolver::getPreferredToolBinaryPath('rector'), 'process']);

$this->processQueue->add(process: $phpCsFixer, label: 'Fixing PHPDoc File Headers with PHP-CS-Fixer');
Expand Down
4 changes: 2 additions & 2 deletions src/Console/Command/RefactorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ protected function configure(): void
->addOption(
name: 'config',
shortcut: 'c',
mode: InputOption::VALUE_OPTIONAL,
mode: InputOption::VALUE_REQUIRED,
description: 'The path to the Rector configuration file.',
default: self::CONFIG
);
Expand Down Expand Up @@ -122,7 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$processBuilder = $this->processBuilder
->withArgument('--ansi')
->withArgument('--config')
->withArgument($this->fileLocator->locate(self::CONFIG));
->withArgument($this->fileLocator->locate($input->getOption('config') ?? self::CONFIG));

if (! $progress) {
$processBuilder = $processBuilder->withArgument('--no-progress-bar');
Expand Down
83 changes: 83 additions & 0 deletions tests/Console/Command/PhpDocCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
use FastForward\DevTools\Filesystem\FilesystemInterface;
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessBuilder;
use FastForward\DevTools\Process\ProcessQueueInterface;
use FastForward\DevTools\Path\ManagedWorkspace;
use FastForward\DevTools\Path\WorkingProjectPathResolver;
Expand All @@ -39,6 +40,7 @@
use FastForward\DevTools\Environment\RuntimeEnvironment;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\Attributes\UsesTrait;
use PHPUnit\Framework\TestCase;
Expand All @@ -61,6 +63,7 @@
#[UsesClass(DevToolsEnvironment::class)]
#[UsesClass(RuntimeEnvironment::class)]
#[CoversClass(PhpDocCommand::class)]
#[UsesClass(ProcessBuilder::class)]
#[UsesClass(Author::class)]
#[UsesClass(Support::class)]
#[UsesClass(ManagedWorkspace::class)]
Expand Down Expand Up @@ -118,6 +121,7 @@ protected function setUp(): void
$this->process->setEnv(Argument::type('array'))
->willReturn($this->process->reveal());

$this->input->getArgument('path')->willReturn([]);
$this->input->getOption('fix')
->willReturn(false);
$this->input->getOption('cache')
Expand Down Expand Up @@ -333,6 +337,85 @@ public function executeWillEnableProgressWhenRequested(): void
self::assertSame(PhpDocCommand::SUCCESS, $this->invokeExecute());
}

/**
* @param list<string> $paths
*/
#[Test]
#[DataProvider('selectedPaths')]
public function executeWillForwardSelectedPathsToBothTools(array $paths): void
{
$queue = $this->prophesize(ProcessQueueInterface::class);
$this->processQueue->add($this->process->reveal(), Argument::cetera())->shouldNotBeCalled();
$this->command = new PhpDocCommand(
new ProcessBuilder(),
$queue->reveal(),
$this->composer->reveal(),
$this->fileLocator->reveal(),
$this->filesystem->reveal(),
$this->renderer->reveal(),
$this->clock->reveal(),
);
$this->input->getArgument('path')->willReturn($paths);
$this->input->getOption('fix')->willReturn(true);
foreach ($paths as $path) {
$this->filesystem->exists($path)->willReturn(true);
}
$suffix = (new Process(['--', ...$paths]))->getCommandLine();
$queue->add(
Argument::that(static fn(Process $process): bool => str_ends_with($process->getCommandLine(), $suffix)
&& str_contains($process->getCommandLine(), '--path-mode=intersection')
&& str_contains($process->getCommandLine(), DevToolsPathResolver::getPreferredToolBinaryPath('php-cs-fixer'))
&& ! str_contains($process->getCommandLine(), '--dry-run')),
false,
false,
'Fixing PHPDoc File Headers with PHP-CS-Fixer',
)->shouldBeCalledOnce();
$queue->add(
Argument::that(static fn(Process $process): bool => str_ends_with($process->getCommandLine(), $suffix)
&& str_contains($process->getCommandLine(), DevToolsPathResolver::getPreferredToolBinaryPath('rector'))
&& ! str_contains($process->getCommandLine(), '--path-mode')
&& ! str_contains($process->getCommandLine(), '--dry-run')),
false,
false,
'Adding Missing PHPDoc with Rector',
)->shouldBeCalledOnce();
$this->filesystem->dumpFile(PhpDocCommand::FILENAME, 'docheader')->shouldBeCalledOnce();
$queue->run($this->output->reveal())->willReturn(PhpDocCommand::SUCCESS);
$this->logger->log('info', 'Checking and fixing PHPDocs...', Argument::type('array'))->shouldBeCalledOnce();
$this->logger->log('info', 'Created .docheader from repository template.', Argument::type('array'))->shouldBeCalledOnce();
$this->logger->log('info', 'PHPDoc checks completed successfully.', Argument::type('array'))->shouldBeCalledOnce();

self::assertSame(PhpDocCommand::SUCCESS, $this->invokeExecute());
}

/**
* @return iterable<string, array{list<string>}>
*/
public static function selectedPaths(): iterable
{
yield 'file' => [['src/Feature.php']];
yield 'directory' => [['src']];
yield 'multiple paths with spaces' => [['source files/Feature.php', 'tests']];
yield 'option-like filename' => [['--rules.php']];
}

/**
* @return void
*/
#[Test]
public function executeWillRejectMissingPathsBeforeWritingOrStartingTools(): void
{
$this->input->getArgument('path')->willReturn(['missing.php']);
$this->filesystem->exists('missing.php')->willReturn(false);
$this->filesystem->dumpFile(Argument::cetera())->shouldNotBeCalled();
$this->processQueue->add($this->process->reveal(), Argument::cetera())->shouldNotBeCalled();
$this->processQueue->run(Argument::cetera())->shouldNotBeCalled();
$this->logger->log('info', 'Checking and fixing PHPDocs...', Argument::type('array'))->shouldBeCalledOnce();
$this->logger->error('PHPDoc path not found: {path}', Argument::type('array'))->shouldBeCalledOnce();

self::assertSame(PhpDocCommand::FAILURE, $this->invokeExecute());
}

/**
* @return int
*/
Expand Down
58 changes: 58 additions & 0 deletions tests/Console/Command/RefactorCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use FastForward\DevTools\Path\DevToolsPathResolver;
use FastForward\DevTools\Path\WorkingProjectPathResolver;
use FastForward\DevTools\Process\ProcessBuilderInterface;
use FastForward\DevTools\Process\ProcessBuilder;
use FastForward\DevTools\Process\ProcessQueueInterface;
use FastForward\DevTools\Container\ContainerFactory;
use FastForward\DevTools\Container\ServiceProvider\DevToolsServiceProvider;
Expand All @@ -40,6 +41,7 @@
use Psr\Log\LoggerInterface;
use FastForward\DevTools\Tests\Container\UsesContainerFactory;
use ReflectionMethod;
use InvalidArgumentException;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -53,6 +55,7 @@
#[CoversClass(RefactorCommand::class)]
#[UsesClass(DevToolsPathResolver::class)]
#[UsesClass(WorkingProjectPathResolver::class)]
#[UsesClass(ProcessBuilder::class)]
#[UsesTrait(LogsCommandResults::class)]
final class RefactorCommandTest extends TestCase
{
Expand Down Expand Up @@ -89,6 +92,7 @@ protected function setUp(): void
$this->logger = $this->prophesize(LoggerInterface::class);
$this->setContainerEntry(LoggerInterface::class, $this->logger->reveal());

$this->input->getOption('config')->willReturn(RefactorCommand::CONFIG);
$this->input->getOption('fix')
->willReturn(false);
$this->input->getOption('progress')
Expand Down Expand Up @@ -211,6 +215,60 @@ public function executeWillEnableProgressWhenRequested(): void
self::assertSame(RefactorCommand::SUCCESS, $this->executeCommand());
}

/**
* @return void
*/
#[Test]
public function executeWillHonorExplicitConfigurationWithSpaces(): void
{
$queue = $this->prophesize(ProcessQueueInterface::class);
$this->processQueue->add($this->process->reveal(), Argument::cetera())->shouldNotBeCalled();
$this->command = new RefactorCommand($this->fileLocator->reveal(), new ProcessBuilder(), $queue->reveal());
$this->input->getOption('config')->willReturn('custom rules/limited.php');
$this->fileLocator->locate('custom rules/limited.php')
->willReturn('/repo/custom rules/limited.php')->shouldBeCalledOnce();
$this->fileLocator->locate(RefactorCommand::CONFIG)->shouldNotBeCalled();
$expected = new Process([
DevToolsPathResolver::getPreferredToolBinaryPath('rector'),
'process',
'--ansi',
'--config',
'/repo/custom rules/limited.php',
'--no-progress-bar',
'--dry-run',
]);
$queue->add(
Argument::that(static fn(Process $process): bool => $expected->getCommandLine() === $process->getCommandLine()),
false,
false,
'Refactoring Code with Rector',
)->shouldBeCalledOnce();
$queue->run($this->output->reveal())->willReturn(RefactorCommand::SUCCESS);
$this->logger->log('info', 'Running Rector for code refactoring...', Argument::type('array'))->shouldBeCalledOnce();
$this->logger->log('info', 'Code refactoring checks completed successfully.', Argument::type('array'))->shouldBeCalledOnce();

self::assertSame(RefactorCommand::SUCCESS, $this->executeCommand());
}

/**
* @return void
*/
#[Test]
public function executeWillRejectInvalidExplicitConfigurationWithoutDefaultFallback(): void
{
$this->input->getOption('config')->willReturn('missing/config.php');
$this->fileLocator->locate('missing/config.php')
->willThrow(new InvalidArgumentException('Configuration not found: missing/config.php'));
$this->fileLocator->locate(RefactorCommand::CONFIG)->shouldNotBeCalled();
$this->processQueue->add($this->process->reveal(), Argument::cetera())->shouldNotBeCalled();
$this->processQueue->run(Argument::cetera())->shouldNotBeCalled();
$this->logger->log('info', 'Running Rector for code refactoring...', Argument::type('array'))->shouldBeCalledOnce();
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Configuration not found: missing/config.php');

$this->executeCommand();
}

/**
* @return int
*/
Expand Down
Loading