From ea44f34366172883038e91938e02da8bbff0ec43 Mon Sep 17 00:00:00 2001 From: smeghead Date: Tue, 25 Mar 2025 22:38:25 +0900 Subject: [PATCH 1/2] feat: add check mode. --- CHANGELOG.md | 9 +++ README.md | 46 ++++++++++++++ src/Command/AbstractCommand.php | 2 + src/Command/CheckCommand.php | 91 ++++++++++++++++++++++++++++ src/Command/ScopesCommand.php | 95 +++-------------------------- src/Command/ScopesTrait.php | 102 ++++++++++++++++++++++++++++++++ src/CommandFactory.php | 27 +++++++++ 7 files changed, 286 insertions(+), 86 deletions(-) create mode 100644 src/Command/CheckCommand.php create mode 100644 src/Command/ScopesTrait.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 639bf00..36f1959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # CHANGELOG +## v0.0.7 (2025-03-25) + +### Features + + * Added new `check` mode with threshold support for CI integration + * Added `--threshold` option to check mode for detecting excessive variable usage + * Returns exit code 2 when variable hard usage exceeds specified threshold + * Restructured code with ScopesTrait to improve maintainability + ### Bug fix * The name of the file path information item in scopes mode is `file`, but I want to match it with `filename`, which is the name of the file path item in single mode. diff --git a/README.md b/README.md index cb606c8..f3e82a3 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,52 @@ The output for scopes mode is a combined report with results sorted by variable } ``` +### CI Integration with Check Mode + +Use the check command with an optional threshold to analyze files and enforce variable usage standards in CI/CD pipelines: + +```bash +# Check files with default threshold (200) +$ vendor/bin/php-variable-hard-usage check src/ + +# Check with custom threshold +$ vendor/bin/php-variable-hard-usage check --threshold=500 src/ tests/ + +# Check specific files and directories +$ vendor/bin/php-variable-hard-usage check --threshold=300 src/Command.php config/ +``` + +The check mode returns different exit codes based on the result: + +* Exit code 0: Success - No analysis errors and no scopes exceeding the threshold +* Exit code 1: Analysis failure - Errors occurred during file parsing or analysis +* Exit code 2: Threshold exceeded - One or more scopes exceeded the specified variable hard usage threshold + +The output includes the threshold used, result status, and a list of scopes that exceeded the threshold: + +```json +{ + "threshold": 500, + "result": "failure", + "scopes": [ + { + "file": "src/Parse/VariableParser.php", + "namespace": "Smeghead\\PhpVariableHardUsage\\Parse", + "name": "VariableParser::collectParseResultPerFunctionLike", + "variableHardUsage": 655 + }, + { + "file": "src/Command/SingleCommand.php", + "namespace": "Smeghead\\PhpVariableHardUsage\\Command", + "name": "SingleCommand::execute", + "variableHardUsage": 530 + } + ] +} +``` + +This mode is particularly useful for integrating the tool into your CI/CD pipeline to fail builds when variable usage exceeds acceptable thresholds. + ### Help and Version Information To display help information: diff --git a/src/Command/AbstractCommand.php b/src/Command/AbstractCommand.php index fcec1d0..ea44949 100644 --- a/src/Command/AbstractCommand.php +++ b/src/Command/AbstractCommand.php @@ -19,8 +19,10 @@ protected function printHelp(): void echo "Commands:\n"; echo " single Analyze a single file\n"; echo " scopes [ ...] Analyze PHP files in directories or specific files\n"; + echo " check [ ...] Check PHP files for hard-coded variables and return non-zero exit code if found\n"; echo "Options:\n"; echo " --help Display help information\n"; echo " --version Show the version of the tool\n"; + echo " --threshold Set the threshold value for reporting (default: 200)\n"; } } \ No newline at end of file diff --git a/src/Command/CheckCommand.php b/src/Command/CheckCommand.php new file mode 100644 index 0000000..b15001d --- /dev/null +++ b/src/Command/CheckCommand.php @@ -0,0 +1,91 @@ + */ + private array $paths; + private int $threshold; + + /** + * @param list $paths ディレクトリまたはファイルのパスリスト + * @param int|null $threshold 閾値 + */ + public function __construct(array $paths, ?int $threshold = null) + { + $this->paths = $paths; + $this->threshold = $threshold ?? 200; // デフォルト閾値は200 + } + + public function execute(): int + { + $analysis = $this->analyzePaths($this->paths); + $results = $analysis['results']; + $hasErrors = $analysis['hasErrors']; + + if (empty($results)) { + return 1; + } + + // 閾値チェックを行い結果を表示 + $exceedingScopes = $this->printResults($results); + + // 閾値を超えるスコープがあればエラーコード2を返す + if (!empty($exceedingScopes)) { + return 2; + } + + // 解析エラーがあればエラーコード1を返す + return $hasErrors ? 1 : 0; + } + + /** + * @param list $results + * @return array 閾値を超えたスコープの配列 + */ + protected function printResults(array $results): array + { + // 閾値を超えるスコープを検出 + $exceedingScopes = []; + + foreach ($results as $result) { + foreach ($result->scopes as $scope) { + $hardUsage = $scope->getVariableHardUsage(); + // 閾値以上の変数の酷使度を持つスコープのみ追加 + if ($hardUsage >= $this->threshold) { + $exceedingScopes[] = [ + 'file' => $result->filename, + 'filename' => $result->filename, + 'namespace' => $scope->namespace, + 'name' => $scope->name, + 'variableHardUsage' => $hardUsage + ]; + } + } + } + + // 酷使度でソート + usort($exceedingScopes, function ($a, $b) { + return $b['variableHardUsage'] <=> $a['variableHardUsage']; + }); + + // レポート作成 + $report = [ + 'threshold' => $this->threshold, + 'result' => empty($exceedingScopes) ? 'success' : 'failure', + 'scopes' => $exceedingScopes + ]; + + // 結果を表示 + echo json_encode($report, JSON_PRETTY_PRINT) . PHP_EOL; + + return $exceedingScopes; + } +} diff --git a/src/Command/ScopesCommand.php b/src/Command/ScopesCommand.php index b2033d0..6a63e85 100644 --- a/src/Command/ScopesCommand.php +++ b/src/Command/ScopesCommand.php @@ -5,12 +5,11 @@ namespace Smeghead\PhpVariableHardUsage\Command; use Smeghead\PhpVariableHardUsage\Analyze\AnalysisResult; -use Smeghead\PhpVariableHardUsage\Analyze\VariableAnalyzer; -use Smeghead\PhpVariableHardUsage\Parse\Exception\ParseFailedException; -use Smeghead\PhpVariableHardUsage\Parse\VariableParser; final class ScopesCommand extends AbstractCommand { + use ScopesTrait; + /** @var list */ private array $paths; @@ -22,67 +21,11 @@ public function __construct(array $paths) $this->paths = $paths; } - /** - * @param list $paths - * @return list - */ - private function pickupPhpFiles(array $paths): array - { - $phpFiles = []; - - // 各パスを処理 - foreach ($paths as $path) { - if (is_dir($path)) { - // ディレクトリの場合は再帰的にPHPファイルを収集 - $dirFiles = $this->findPhpFiles($path); - $phpFiles = array_merge($phpFiles, $dirFiles); - } elseif (is_file($path) && pathinfo($path, PATHINFO_EXTENSION) === 'php') { - // 単一のPHPファイルの場合 - $phpFiles[] = $path; - } else { - fwrite(STDERR, "Invalid path: {$path}\n"); - } - } - - return $phpFiles; - } - - private function analyzeFile(string $file): AnalysisResult - { - $parser = new VariableParser(); - $content = file_get_contents($file); - if ($content === false) { - throw new ParseFailedException("Failed to read file: {$file}"); - } - - $parseResult = $parser->parse($content); - $analyzer = new VariableAnalyzer($file, $parseResult->functions); - return $analyzer->analyze(); - } - public function execute(): int { - $phpFiles = $this->pickupPhpFiles($this->paths); - - if (empty($phpFiles)) { - fwrite(STDERR, "No PHP files found in specified paths\n"); - return 1; - } - - // 重複を削除 - $phpFiles = array_unique($phpFiles); - - $results = []; - $hasErrors = false; - - foreach ($phpFiles as $file) { - try { - $results[] = $this->analyzeFile($file); - } catch (\Exception $e) { - fwrite(STDERR, "Error analyzing {$file}: {$e->getMessage()}\n"); - $hasErrors = true; - } - } + $analysis = $this->analyzePaths($this->paths); + $results = $analysis['results']; + $hasErrors = $analysis['hasErrors']; if (empty($results)) { return 1; @@ -96,37 +39,17 @@ public function execute(): int } /** - * @return list - */ - private function findPhpFiles(string $directory): array - { - $result = []; - $files = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS) - ); - - /** @var \SplFileInfo $file */ - foreach ($files as $file) { - if ($file->isFile() && $file->getExtension() === 'php') { - $result[] = $file->getPathname(); - } - } - - return $result; - } - - /** - * @param list<\Smeghead\PhpVariableHardUsage\Analyze\AnalysisResult> $results + * @param list $results */ - private function printResults(array $results): void + protected function printResults(array $results): void { // スコープベースのレポートを生成 $allScopes = []; foreach ($results as $result) { foreach ($result->scopes as $scope) { $allScopes[] = [ - 'file' => $result->filename, // 既存の 'file' プロパティを維持 - 'filename' => $result->filename, // 新しく 'filename' プロパティを追加 + 'file' => $result->filename, + 'filename' => $result->filename, 'namespace' => $scope->namespace, 'name' => $scope->name, 'variableHardUsage' => $scope->getVariableHardUsage() diff --git a/src/Command/ScopesTrait.php b/src/Command/ScopesTrait.php new file mode 100644 index 0000000..7f27978 --- /dev/null +++ b/src/Command/ScopesTrait.php @@ -0,0 +1,102 @@ + $paths + * @return list + */ + private function pickupPhpFiles(array $paths): array + { + $phpFiles = []; + + // 各パスを処理 + foreach ($paths as $path) { + if (is_dir($path)) { + // ディレクトリの場合は再帰的にPHPファイルを収集 + $dirFiles = $this->findPhpFiles($path); + $phpFiles = array_merge($phpFiles, $dirFiles); + } elseif (is_file($path) && pathinfo($path, PATHINFO_EXTENSION) === 'php') { + // 単一のPHPファイルの場合 + $phpFiles[] = $path; + } else { + fwrite(STDERR, "Invalid path: {$path}\n"); + } + } + + return $phpFiles; + } + + private function analyzeFile(string $file): AnalysisResult + { + $parser = new VariableParser(); + $content = file_get_contents($file); + if ($content === false) { + throw new ParseFailedException("Failed to read file: {$file}"); + } + + $parseResult = $parser->parse($content); + $analyzer = new VariableAnalyzer($file, $parseResult->functions); + return $analyzer->analyze(); + } + + /** + * @return list + */ + private function findPhpFiles(string $directory): array + { + $result = []; + $files = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS) + ); + + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $result[] = $file->getPathname(); + } + } + + return $result; + } + + /** + * @param list $paths + * @return array{results: list, hasErrors: bool} + */ + protected function analyzePaths(array $paths): array + { + $phpFiles = $this->pickupPhpFiles($paths); + + if (empty($phpFiles)) { + fwrite(STDERR, "No PHP files found in specified paths\n"); + return ['results' => [], 'hasErrors' => true]; + } + + // 重複を削除 + $phpFiles = array_unique($phpFiles); + + $results = []; + $hasErrors = false; + + foreach ($phpFiles as $file) { + try { + $results[] = $this->analyzeFile($file); + } catch (\Exception $e) { + fwrite(STDERR, "Error analyzing {$file}: {$e->getMessage()}\n"); + $hasErrors = true; + } + } + + return ['results' => $results, 'hasErrors' => $hasErrors]; + } +} \ No newline at end of file diff --git a/src/CommandFactory.php b/src/CommandFactory.php index a8c09a7..8a5937a 100644 --- a/src/CommandFactory.php +++ b/src/CommandFactory.php @@ -9,6 +9,7 @@ use Smeghead\PhpVariableHardUsage\Command\SingleCommand; use Smeghead\PhpVariableHardUsage\Command\ScopesCommand; use Smeghead\PhpVariableHardUsage\Command\VersionCommand; +use Smeghead\PhpVariableHardUsage\Command\CheckCommand; final class CommandFactory { @@ -48,6 +49,32 @@ public function createCommand(array $argv): CommandInterface 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]); } From 042369f6b9904c36560c6fe4649e49e58760d695 Mon Sep 17 00:00:00 2001 From: smeghead Date: Tue, 25 Mar 2025 22:44:52 +0900 Subject: [PATCH 2/2] fix: fix phpstan error. --- src/Command/CheckCommand.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Command/CheckCommand.php b/src/Command/CheckCommand.php index b15001d..bf46eb2 100644 --- a/src/Command/CheckCommand.php +++ b/src/Command/CheckCommand.php @@ -38,7 +38,7 @@ public function execute(): int $exceedingScopes = $this->printResults($results); // 閾値を超えるスコープがあればエラーコード2を返す - if (!empty($exceedingScopes)) { + if (!empty($exceedingScopes['scopes'])) { return 2; } @@ -48,7 +48,17 @@ public function execute(): int /** * @param list $results - * @return array 閾値を超えたスコープの配列 + * @return array{ + * threshold: int, + * result: string, + * scopes: list + * } 閾値を超えたスコープの配列 */ protected function printResults(array $results): array { @@ -86,6 +96,6 @@ protected function printResults(array $results): array // 結果を表示 echo json_encode($report, JSON_PRETTY_PRINT) . PHP_EOL; - return $exceedingScopes; + return $report; } }