mirror of
https://github.com/ivuorinen/branch-usage-checker.git
synced 2026-02-24 07:51:58 +00:00
chore: cleanup unused deps and dead code (#43)
* chore: remove unused code and dead files
Remove InspireCommand, GitHubApiBranch DTO, PackagistApiStatsPayload DTO,
and GitHubRestApi fetcher. Remove InspireCommand reference from config.
* chore(deps): remove unused composer dependencies
Remove guzzlehttp/guzzle, laravel-zero/phar-updater, and
nunomaduro/termwind. Clean up dead autoload entries and orphaned
allow-plugins configuration.
* feat: improve CheckCommand input handling and error reporting
Add vendor/package slash format support, input validation, HTTP error
handling, and concurrent stats fetching. Replace exit() calls with
return statements. Add 13 new test cases with Http::fake coverage.
* build: rebuild PHAR executable
* chore: add CLAUDE.md project instructions
* docs: add design for PHPCS + CaptainHook integration
* docs: add implementation plan for PHPCS + CaptainHook
* build(deps): add squizlabs/php_codesniffer
* build: add composer lint and format scripts
* build(deps): add captainhook and hook-installer
* build: configure CaptainHook pre-commit hook for PHPCS
* docs: add lint/format commands and hook info to CLAUDE.md
* build: add markdownlint-cli2 configuration
* build: add lint:md, lint:ec, lint:all, and format:md composer scripts
* build: add editorconfig and markdownlint to pre-commit hook
* docs: add new lint and format commands to CLAUDE.md
* chore: add missing gitignore patterns
* refactor: replace Http facade with injected HttpFactory
- Resolve HttpFactory via the container instead of using the Http facade
directly, improving testability and explicitness
- Simplify early-return logic in package metadata error handling
- Reformat long strings and closures for PSR-12 line length compliance
- Extract repeated stats URL prefix into variable in tests
* build: add Claude Code shared settings and skills
Add shared hooks (auto-format PHP on edit, block vendor/lock edits),
shared permissions for common dev commands, and two user-invocable
skills (/build-phar, /release-check).
* refactor: extract test constants to reduce duplication in CheckCommandTest
Replace 6 occurrences of the command string and 4 occurrences of the
stats URL with TEST_COMMAND and TEST_STATS_URL constants. Fixes
SonarCloud S1192 code smells and brings duplicated lines density
below the 3% threshold.
* refactor: extract resolveInput and fetchPackageMetadata from handle()
Reduce handle() from 8 return statements to 3 by extracting input
validation into resolveInput() and HTTP fetching into
fetchPackageMetadata(). Fixes SonarCloud S1142 (too many returns).
* fix: improve CheckCommand error handling, input normalization, and timeouts
- Return exit code 1 from catch block instead of silently succeeding
- Normalize vendor/package inputs to lowercase before validation
- Add 10-second HTTP timeouts to metadata fetch and pool requests
- Return false from outputSuggestions when no suggestions found
* test: stub HTTP in live-request tests and assert exception output
- Add Http::fake() to slash-format and two-argument tests to prevent
real Packagist requests during CI
- Assert exception message in try-block test and expect exit code 1
to match the updated catch behavior
* docs: fix command signature, remove stale Fetchers reference in CLAUDE.md
- Change {package} to {package?} and note vendor/package combined form
- Remove non-existent app/Fetchers/ directory reference
- Update HTTP dependency note to reflect HttpFactory injection
* fix: guard array_combine against mismatched Packagist stats
Add a length check before array_combine() so malformed stats
(different label/value counts) produce a warning and skip the
branch instead of throwing a ValueError.
* refactor: change outputSuggestions return type to void
No caller uses the return value. Replace bool return type with
void and remove the now-unnecessary return statements.
* fix: narrow Throwable catch to Exception, add docblocks, remove unused default
- Catch \Exception instead of \Throwable so programming errors (TypeError,
OutOfMemoryError) propagate instead of being silently displayed
- Add minimal PHPDoc summaries to all CheckCommand methods for coverage
- Remove unused default `= []` on outputSuggestions parameter
- Update test to expect TypeError propagation via ->throws()
* refactor: extract constants, fix dead store, deduplicate test setup
- Add TIMEOUT_SECONDS and PACKAGIST_URL constants in CheckCommand
- Replace inline timeout(10) and URL strings with constant references
- Fix dead store: $deletable[$k] = $values['Total'] → $deletable[] = $k
- Eliminate unused $keys intermediate variable in outputSuggestions()
- Standardize string formatting to use sprintf() consistently
- Add TEST_METADATA_URL constant and fakePackageResponses() helper in tests
- Refactor 6 tests to use shared helper, reducing Http::fake duplication
* docs: fix capitalization and remove stale GitHub reference in CLAUDE.md
* docs: add design for fixing SonarCloud duplication quality gate
* docs: add implementation plan for SonarCloud duplication fix
* refactor(tests): parameterize input-validation tests to fix duplication gate
* feat: add PHPMD linter and fix Codacy markdownlint config
Add PHPMD as a dev dependency with cleancode, codesize, unusedcode, and
naming rulesets. Integrate via composer lint:phpmd script, lint:all, and
CaptainHook pre-commit hook.
Split markdownlint config into .markdownlint.jsonc (rules) and
.markdownlint-cli2.jsonc (ignores only) so Codacy's plain markdownlint
can discover the shared rules file.
* fix: rename markdownlint config to .json and disable MD043
Rename .markdownlint.jsonc to .markdownlint.json so Codacy discovers
it at highest priority, and add required-headings: false to explicitly
disable MD043 which flags docs with varying heading structures.
* fix: guard against ConnectionException in pool responses and catch Throwable
Pool responses can be Throwable (e.g. ConnectionException) instead of
Response objects, causing crashes on ->failed(). Add instanceof guard,
widen catch to Throwable with TypeError re-throw, and extract loop into
collectBranchStats() to stay under cyclomatic complexity threshold.
This commit is contained in:
@@ -3,14 +3,15 @@
|
||||
namespace App\Commands;
|
||||
|
||||
use App\Dto\PackagistApiPackagePayload;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\Factory as HttpFactory;
|
||||
use Illuminate\Http\Client\Pool;
|
||||
use LaravelZero\Framework\Commands\Command;
|
||||
|
||||
class CheckCommand extends Command
|
||||
{
|
||||
protected $signature = 'check
|
||||
{vendor : Package vendor (required)}
|
||||
{package : Package name (required)}
|
||||
{vendor : Package vendor or vendor/package}
|
||||
{package? : Package name}
|
||||
{months=9 : How many months should we return for review (optional)}
|
||||
';
|
||||
protected $description = 'Check package branch usage';
|
||||
@@ -20,23 +21,30 @@ class CheckCommand extends Command
|
||||
private string $filter = '';
|
||||
private int $totalBranches = 0;
|
||||
|
||||
private const NAME_PATTERN = '/^[a-z0-9]([_.\-]?[a-z0-9]+)*$/';
|
||||
private const TIMEOUT_SECONDS = 10;
|
||||
private const PACKAGIST_URL = 'https://packagist.org/packages/%s/%s';
|
||||
|
||||
private HttpFactory $http;
|
||||
|
||||
/** Execute the check command. */
|
||||
public function handle(): int
|
||||
{
|
||||
$this->vendor = (string)$this->argument('vendor');
|
||||
$this->package = (string)$this->argument('package');
|
||||
$months = (int)$this->argument('months');
|
||||
$this->http = resolve(HttpFactory::class);
|
||||
|
||||
$this->info('Checking: ' . sprintf('%s/%s', $this->vendor, $this->package));
|
||||
if (!$this->resolveInput()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$months = (int) $this->argument('months');
|
||||
|
||||
$this->info(sprintf('Checking: %s/%s', $this->vendor, $this->package));
|
||||
$this->info('Months: ' . $months);
|
||||
|
||||
$payload = Http::get(
|
||||
sprintf(
|
||||
'https://packagist.org/packages/%s/%s.json',
|
||||
$this->vendor,
|
||||
$this->package
|
||||
)
|
||||
);
|
||||
|
||||
$payload = $this->fetchPackageMetadata();
|
||||
if ($payload === null) {
|
||||
return 1;
|
||||
}
|
||||
$this->filter = now()->subMonths($months)->day(1)->toDateString();
|
||||
|
||||
try {
|
||||
@@ -45,9 +53,9 @@ class CheckCommand extends Command
|
||||
|
||||
$versions = collect($pkg->versions ?? [])
|
||||
->keys()
|
||||
// Filter actual versions out.
|
||||
->filter(fn ($version) => \str_starts_with($version, 'dev-'))
|
||||
->sort();
|
||||
->sort()
|
||||
->values();
|
||||
|
||||
$this->totalBranches = $versions->count();
|
||||
|
||||
@@ -58,48 +66,150 @@ class CheckCommand extends Command
|
||||
)
|
||||
);
|
||||
|
||||
$statistics = collect($versions)
|
||||
->mapWithKeys(fn ($branch) => $this->getStatistics($branch))
|
||||
->toArray();
|
||||
$responses = $this->http->pool(
|
||||
fn (Pool $pool) => $versions->map(
|
||||
fn ($branch) => $pool->as($branch)->timeout(self::TIMEOUT_SECONDS)->get($this->getStatsUrl($branch))
|
||||
)->toArray()
|
||||
);
|
||||
|
||||
$statistics = $this->collectBranchStats($versions, $responses);
|
||||
|
||||
$this->info('Downloaded statistics...');
|
||||
|
||||
$this->outputTable($statistics);
|
||||
$this->outputSuggestions($statistics);
|
||||
} catch (\Exception $e) {
|
||||
$this->error($e->getMessage(), $e);
|
||||
if ($this->outputTable($statistics)) {
|
||||
$this->outputSuggestions($statistics);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if ($e instanceof \TypeError) {
|
||||
throw $e;
|
||||
}
|
||||
$this->error($e->getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getStatistics(string $branch): array
|
||||
/** Parse and validate vendor/package input arguments. */
|
||||
private function resolveInput(): bool
|
||||
{
|
||||
$payload = Http::get(
|
||||
$vendor = strtolower((string) $this->argument('vendor'));
|
||||
$package = $this->argument('package');
|
||||
|
||||
if (str_contains($vendor, '/')) {
|
||||
if ($package !== null) {
|
||||
$this->error(
|
||||
'Conflicting arguments: vendor/package format'
|
||||
. ' and separate package argument cannot be used together.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
[$vendor, $package] = explode('/', $vendor, 2);
|
||||
}
|
||||
|
||||
if ($package === null || $package === '') {
|
||||
$this->error('Missing package name. Usage: check vendor/package or check vendor package');
|
||||
return false;
|
||||
}
|
||||
|
||||
$package = strtolower((string) $package);
|
||||
|
||||
if (!preg_match(self::NAME_PATTERN, $vendor)) {
|
||||
$this->error("Invalid vendor name: {$vendor}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!preg_match(self::NAME_PATTERN, $package)) {
|
||||
$this->error("Invalid package name: {$package}");
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->vendor = $vendor;
|
||||
$this->package = $package;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Fetch package metadata from Packagist. */
|
||||
private function fetchPackageMetadata(): ?\Illuminate\Http\Client\Response
|
||||
{
|
||||
$payload = $this->http->timeout(self::TIMEOUT_SECONDS)->get(
|
||||
sprintf(
|
||||
'https://packagist.org/packages/%s/%s/stats/%s.json?average=monthly&from=%s',
|
||||
self::PACKAGIST_URL . '.json',
|
||||
$this->vendor,
|
||||
$this->package,
|
||||
$branch,
|
||||
$this->filter
|
||||
$this->package
|
||||
)
|
||||
);
|
||||
|
||||
$data = collect($payload->json());
|
||||
$labels = collect($data->get('labels', []))->toArray();
|
||||
$values = collect($data->get('values', []))->flatten()->toArray();
|
||||
if ($payload->failed()) {
|
||||
if ($payload->status() === 404) {
|
||||
$this->error("Package not found: {$this->vendor}/{$this->package}");
|
||||
return null;
|
||||
}
|
||||
$this->error("Failed to fetch package metadata (HTTP {$payload->status()})");
|
||||
return null;
|
||||
}
|
||||
|
||||
$labels[] = 'Total';
|
||||
$values[] = array_sum($values);
|
||||
|
||||
return [$branch => \array_combine($labels, $values)];
|
||||
return $payload;
|
||||
}
|
||||
|
||||
private function outputTable(array $statistics): void
|
||||
/** Build the Packagist stats API URL for a branch. */
|
||||
private function getStatsUrl(string $branch): string
|
||||
{
|
||||
return sprintf(
|
||||
self::PACKAGIST_URL . '/stats/%s.json?average=monthly&from=%s',
|
||||
$this->vendor,
|
||||
$this->package,
|
||||
$branch,
|
||||
$this->filter
|
||||
);
|
||||
}
|
||||
|
||||
/** Parse pooled responses into branch download statistics. */
|
||||
private function collectBranchStats($versions, array $responses): array
|
||||
{
|
||||
$statistics = [];
|
||||
foreach ($versions as $branch) {
|
||||
$response = $responses[$branch];
|
||||
|
||||
if ($response instanceof \Throwable) {
|
||||
$this->warn("Failed to fetch stats for {$branch}, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($response->failed()) {
|
||||
$this->warn("Failed to fetch stats for {$branch} (HTTP {$response->status()}), skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = collect($response->json());
|
||||
$labels = collect($data->get('labels', []))->toArray();
|
||||
$values = collect($data->get('values', []))->flatten()->toArray();
|
||||
|
||||
$labels[] = 'Total';
|
||||
$values[] = array_sum($values);
|
||||
|
||||
if (count($labels) !== count($values)) {
|
||||
$this->warn(sprintf(
|
||||
'Malformed stats for %s (labels: %d, values: %d), skipping.',
|
||||
$branch,
|
||||
count($labels),
|
||||
count($values)
|
||||
));
|
||||
continue;
|
||||
}
|
||||
$statistics[$branch] = \array_combine($labels, $values);
|
||||
}
|
||||
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
/** Render the download statistics table. */
|
||||
private function outputTable(array $statistics): bool
|
||||
{
|
||||
if (empty($statistics)) {
|
||||
$this->info('No statistics found... Stopping.');
|
||||
exit(0);
|
||||
return false;
|
||||
}
|
||||
|
||||
$tableHeaders = ['' => 'Branch'];
|
||||
@@ -107,44 +217,41 @@ class CheckCommand extends Command
|
||||
|
||||
foreach ($statistics as $branch => $stats) {
|
||||
foreach ($stats as $m => $v) {
|
||||
$tableHeaders[$m] = (string)$m;
|
||||
$tableHeaders[$m] = (string) $m;
|
||||
$tableBranches[$branch][$branch] = $branch;
|
||||
$tableBranches[$branch][$m] = (string)$v;
|
||||
$tableBranches[$branch][$m] = (string) $v;
|
||||
}
|
||||
}
|
||||
|
||||
$this->line('');
|
||||
$this->table($tableHeaders, $tableBranches);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function outputSuggestions(array $statistics = []): void
|
||||
/** Render suggestions for zero-download branches. */
|
||||
private function outputSuggestions(array $statistics): void
|
||||
{
|
||||
$deletable = [];
|
||||
if (empty($statistics)) {
|
||||
$this->info('No statistics to give suggestions for. Quitting...');
|
||||
exit(0);
|
||||
}
|
||||
|
||||
foreach ($statistics as $k => $values) {
|
||||
if (!empty($values['Total'])) {
|
||||
continue;
|
||||
}
|
||||
$deletable[$k] = $values['Total'];
|
||||
$deletable[] = $k;
|
||||
}
|
||||
|
||||
if (empty($deletable)) {
|
||||
$this->info('No suggestions available. Good job!');
|
||||
exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
$keys = array_keys($deletable);
|
||||
|
||||
$branches = collect($keys)->mapWithKeys(function ($branch) {
|
||||
$branches = collect($deletable)->mapWithKeys(function ($branch) {
|
||||
return [
|
||||
$branch => [
|
||||
$branch,
|
||||
sprintf(
|
||||
'https://packagist.org/packages/%s/%s#%s',
|
||||
self::PACKAGIST_URL . '#%s',
|
||||
$this->vendor,
|
||||
$this->package,
|
||||
$branch
|
||||
|
||||
Reference in New Issue
Block a user