Skip to content

feat: add check mode. #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 25, 2025
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions src/Command/AbstractCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ protected function printHelp(): void
echo "Commands:\n";
echo " single <file> Analyze a single file\n";
echo " scopes <path1> [<path2> ...] Analyze PHP files in directories or specific files\n";
echo " check <path1> [<path2> ...] 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 <number> Set the threshold value for reporting (default: 200)\n";
}
}
101 changes: 101 additions & 0 deletions src/Command/CheckCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Smeghead\PhpVariableHardUsage\Command;

use Smeghead\PhpVariableHardUsage\Analyze\AnalysisResult;

final class CheckCommand extends AbstractCommand
{
use ScopesTrait;

/** @var list<string> */
private array $paths;
private int $threshold;

/**
* @param list<string> $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['scopes'])) {
return 2;
}

// 解析エラーがあればエラーコード1を返す
return $hasErrors ? 1 : 0;
}

/**
* @param list<AnalysisResult> $results
* @return array{
* threshold: int,
* result: string,
* scopes: list<array{
* file: string,
* filename: string,
* namespace: string|null,
* name: string,
* variableHardUsage: int
* }>
* } 閾値を超えたスコープの配列
*/
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 $report;
}
}
95 changes: 9 additions & 86 deletions src/Command/ScopesCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> */
private array $paths;

Expand All @@ -22,67 +21,11 @@ public function __construct(array $paths)
$this->paths = $paths;
}

/**
* @param list<string> $paths
* @return list<string>
*/
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;
Expand All @@ -96,37 +39,17 @@ public function execute(): int
}

/**
* @return list<string>
*/
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<AnalysisResult> $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()
Expand Down
Loading