diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index ad630d0..aaeb0f5 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -40,3 +40,6 @@ jobs: - name: Run PHPStan run: composer run-script phpstan + + - name: Run php-variable-hard-usage + run: composer run-script php-variable-hard-usage diff --git a/composer.json b/composer.json index b6125e8..572640d 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,9 @@ ], "phpstan": [ "vendor/bin/phpstan analyse" + ], + "php-variable-hard-usage": [ + "php bin/php-variable-hard-usage check --threshold=300 src/" ] }, "bin": [ diff --git a/src/Command.php b/src/Command.php index 0796281..1805305 100644 --- a/src/Command.php +++ b/src/Command.php @@ -4,6 +4,8 @@ namespace Smeghead\PhpVariableHardUsage; +use Smeghead\PhpVariableHardUsage\Option\CommandFactory; + final class Command { /** @@ -12,8 +14,8 @@ final class Command */ public function run(array $argv): int { - $factory = new CommandFactory(); - $command = $factory->createCommand($argv); + $factory = new CommandFactory($argv); + $command = $factory->create(); return $command->execute(); } } \ No newline at end of file diff --git a/src/CommandFactory.php b/src/CommandFactory.php deleted file mode 100644 index 8a5937a..0000000 --- a/src/CommandFactory.php +++ /dev/null @@ -1,81 +0,0 @@ - $argv - */ - public function createCommand(array $argv): CommandInterface - { - if (count($argv) < 2) { - return new HelpCommand(); - } - - $arg = $argv[1]; - - if ($arg === '--help') { - return new HelpCommand(); - } - - if ($arg === '--version') { - return new VersionCommand(); - } - - if ($arg === 'single') { - if (count($argv) < 3) { - fwrite(STDERR, "Usage: php bin/php-variable-hard-usage single \n"); - return new HelpCommand(); - } - return new SingleCommand($argv[2]); - } - - if ($arg === 'scopes') { - if (count($argv) < 3) { - fwrite(STDERR, "Usage: php bin/php-variable-hard-usage scopes [ ...]\n"); - return new HelpCommand(); - } - // 複数のパスを渡す - return new ScopesCommand(array_slice($argv, 2)); - } - - if ($arg === 'check') { - if (count($argv) < 3) { - fwrite(STDERR, "Usage: php bin/php-variable-hard-usage check [--threshold=] [ ...]\n"); - return new HelpCommand(); - } - - // 残りの引数を解析 - $threshold = null; - $paths = []; - - foreach (array_slice($argv, 2) as $argument) { - if (preg_match('/^--threshold=(\d+)$/', $argument, $matches)) { - $threshold = (int)$matches[1]; - } else { - $paths[] = $argument; - } - } - - if (empty($paths)) { - fwrite(STDERR, "Usage: php bin/php-variable-hard-usage check [--threshold=] [ ...]\n"); - return new HelpCommand(); - } - - return new CheckCommand($paths, $threshold); - } - - // 後方互換性のため、コマンドが指定されていない場合は単一ファイルモードとして扱う - return new SingleCommand($argv[1]); - } -} \ No newline at end of file diff --git a/src/Option/CommandFactory.php b/src/Option/CommandFactory.php new file mode 100644 index 0000000..7b6eeb6 --- /dev/null +++ b/src/Option/CommandFactory.php @@ -0,0 +1,223 @@ + */ + private array $argv; + + /** + * @param array $argv コマンドライン引数 + */ + public function __construct(array $argv) + { + $this->argv = $argv; + } + + /** + * コマンドライン引数を解析し、コマンドと引数を返す + */ + public function create(): CommandInterface + { + // 引数がない場合はヘルプコマンド + if (count($this->argv) < 2) { + return new HelpCommand(); + } + + $command = $this->argv[1]; + + // ヘルプと バージョン表示は特別処理 + if ($command === '--help') { + return new HelpCommand(); + } + + if ($command === '--version') { + return new VersionCommand(); + } + + // コマンドに応じた処理 + switch ($command) { + case 'single': + return $this->parseSingleCommand(); + + case 'scopes': + return $this->parseScopesCommand(); + + case 'check': + return $this->parseCheckCommand(); + + default: + // 後方互換性のため、引数そのものをファイル名として解釈 + return new SingleCommand($command); + } + } + + /** + * 単一ファイルコマンドを解析 + */ + private function parseSingleCommand(): CommandInterface + { + $args = array_slice($this->argv, 2); + + if (empty($args)) { + return new HelpCommand(); + } + + return new SingleCommand($args[0]); + } + + /** + * スコープコマンドを解析 + */ + private function parseScopesCommand(): CommandInterface + { + $args = array_slice($this->argv, 2); + + if (empty($args)) { + return new HelpCommand(); + } + + return new ScopesCommand($args); + } + + /** + * チェックコマンドを解析 + */ + private function parseCheckCommand(): CommandInterface + { + $args = array_slice($this->argv, 2); + + if (empty($args)) { + return new HelpCommand(); + } + + $parsedArgs = $this->parseArguments($args); + + if (empty($parsedArgs->paths)) { + return new HelpCommand(); + } + + $threshold = isset($parsedArgs->options['threshold']) ? intval($parsedArgs->options['threshold']) : null; + + return new CheckCommand($parsedArgs->paths, $threshold); + } + + /** + * コマンドライン引数を解析して、オプションとパスに分離する + * + * @param array $args + * @return ParsedArguments + */ + private function parseArguments(array $args): ParsedArguments + { + $options = []; + $paths = []; + + $i = 0; + while ($i < count($args)) { + $arg = $args[$i]; + + if ($this->isOptionWithValue($arg, '--threshold', $args, $i)) { + $options['threshold'] = (int)$args[$i + 1]; + $i += 2; + } elseif ($this->isOptionWithInlineValue($arg, '--threshold=', $matches)) { + $options['threshold'] = (int)$matches[1]; + $i++; + } elseif ($this->isOption($arg)) { + [$name, $value] = $this->parseOption($arg); + $options[$name] = $value; + $i++; + } else { + $paths[] = $arg; + $i++; + } + } + + return new ParsedArguments($paths, $options); + } + + /** + * 値を持つオプションかどうかを判定 + * + * @param string $arg 現在の引数 + * @param string $optionName オプション名 + * @param array $args 全引数 + * @param int $index 現在の位置 + * @return bool + */ + private function isOptionWithValue(string $arg, string $optionName, array $args, int $index): bool + { + return $arg === $optionName && isset($args[$index + 1]); + } + + /** + * インライン値を持つオプションかどうかを判定 + * + * @param string $arg 現在の引数 + * @param string $prefix オプションのプレフィックス + * @param null &$matches 正規表現のマッチ結果を格納する変数 + * @return bool + */ + private function isOptionWithInlineValue(string $arg, string $prefix, &$matches): bool + { + return preg_match('/^' . preg_quote($prefix, '/') . '(\d+)$/', $arg, $matches) === 1; + } + + /** + * オプションかどうかを判定 + * + * @param string $arg 現在の引数 + * @return bool + */ + private function isOption(string $arg): bool + { + return strpos($arg, '--') === 0; + } + + /** + * オプション文字列をパースして名前と値を取得 + * + * @param string $option オプション文字列 + * @return array{0: string, 1: string|bool} [オプション名, オプション値] + */ + private function parseOption(string $option): array + { + $optName = substr($option, 2); + + if (strpos($optName, '=') !== false) { + [$name, $value] = explode('=', $optName, 2); + return [$name, $value]; + } + + return [$optName, true]; + } +} + +/** + * パース済みの引数を表すクラス + */ +final class ParsedArguments +{ + /** + * @param array $paths パスのリスト + * @param array $options オプションのマップ + */ + public function __construct( + public readonly array $paths, + public readonly array $options + ) { + } +} \ No newline at end of file diff --git a/test/CommandFactoryTest.php b/test/CommandFactoryTest.php new file mode 100644 index 0000000..8f1a5ce --- /dev/null +++ b/test/CommandFactoryTest.php @@ -0,0 +1,62 @@ +create(); + $this->assertInstanceOf(HelpCommand::class, $result); + } + + public function testParseHelp(): void + { + $argv = ['script.php', '--help']; + $sut = new CommandFactory($argv); + $result = $sut->create(); + $this->assertInstanceOf(HelpCommand::class, $result); + } + + public function testParseVersion(): void + { + $argv = ['script.php', '--version']; + $sut = new CommandFactory($argv); + $result = $sut->create(); + $this->assertInstanceOf(VersionCommand::class, $result); + } + + public function testParseSingle(): void + { + $argv = ['script.php', 'single', 'file.php']; + $sut = new CommandFactory($argv); + $result = $sut->create(); + $this->assertInstanceOf(SingleCommand::class, $result); + } + + public function testParseScopes(): void + { + $argv = ['script.php', 'scopes', 'dir1', 'dir2']; + $sut = new CommandFactory($argv); + $result = $sut->create(); + $this->assertInstanceOf(ScopesCommand::class, $result); + } + + public function testParseCheck(): void + { + $argv = ['script.php', 'check', '--threshold', '200', 'dir1', 'dir2']; + $sut = new CommandFactory($argv); + $result = $sut->create(); + $this->assertInstanceOf(CheckCommand::class, $result); + } +} \ No newline at end of file