Hacky php file to do some auto-checks for pull requests

May 20, 2024 note-to-self

I wrote this hacky PHP file to assist me in doing pull request reviews as well as to check my own, to make the best use of my team's time. These are just things I wanted to examine every time without having to do much work. We're on the path to CI, but we aren't quite there yet, so we're still doing a few things manually.

<?php

$returnPath = __DIR__;

$choices = [
    'site-1' => new Choice(name: 'site-1', path: '/Users/ha/Code/site-1', main: 'main'),
    'site-2' => new Choice(name: 'site-2', path: '/Users/ha/Code/site-2', main: 'main'),
    'site-3' => new Choice(name: 'site-3', path: '/Users/ha/Code/site-3', main: 'main'),
];

$inChoice = $argv[1] ?? null;
$choice = $choices[$inChoice] ?? null;

if (is_null($choice)) {
    $choice = '<empty>';
    die("Make a choice! `{$choice}` is not right!" . PHP_EOL);
}

chdir($choice->path);

$changedFiles = getChangedFiles($choice);

if (!sizeof($changedFiles)) {
    die("There aren't any modified files to check." . PHP_EOL);
}

echo "FILES WITHOUT ENDING NEWLINE" . PHP_EOL;
echo "\n\t" . implode("\n\t", checkFilesNotEndingInNewLine($changedFiles)) . PHP_EOL;


// Improve: dirname and compare
echo PHP_EOL;
echo "EYEBALL NAMESPACES";
echo "\n\t" . implode("\n\t", grep($changedFiles, 'namespace')) . PHP_EOL;

echo PHP_EOL;
echo "EYEBALL USE STATEMENTS";
echo "\n\t" . implode("\n\t", grep($changedFiles, 'use [^;]*;')) . PHP_EOL;

echo PHP_EOL;
echo "EXAMINE EXCEPTIONS THROWN (CUSTOMIZABLE?)" . PHP_EOL;
echo <<<EXC
    - Look for exceptions that might be more customizable, or are throwing the right kind
    - Look for exceptions that don't have `@throws` annotation
    - Rethrown exceptions should include previous exception!
EXC;
echo PHP_EOL;
echo "\n\t" . implode("\n\t", grep($changedFiles, 'throw new')) . PHP_EOL;

echo PHP_EOL;
echo "ANY TODOs?";
echo "\n\t" . implode("\n\t", grep($changedFiles, '\bTODO\b')) . PHP_EOL;

echo PHP_EOL;
echo "ANY DEBUGGING CODE?";
echo PHP_EOL . "\t" . implode("\n\t", grep($changedFiles, '\bconsole\.log\b')) . PHP_EOL;
echo PHP_EOL . "\t" . implode("\n\t", grep($changedFiles, '\bprint_r\b')) . PHP_EOL;

echo PHP_EOL;
echo "ANY SWITCH STATEMENTS?";
echo PHP_EOL . "\t" . implode("\n\t", grep($changedFiles, '\bswitch\b')) . PHP_EOL;

echo "CODESNIFFER-DETECTED ISSUES" . PHP_EOL;
$codeSnifferDiffFiles = codeSnifferDiff($changedFiles);
echo PHP_EOL . "\t" . implode("\n\t", $codeSnifferDiffFiles) . PHP_EOL;

chdir($returnPath);

if (sizeof($codeSnifferDiffFiles)) {
    echo "To mass fix:" . PHP_EOL;
    foreach ($changedFiles as $file => $status) {
        echo "/Users/ha/Code/bin/php-cs-fixer/vendor/bin/php-cs-fixer fix --using-cache no --rules @PSR12 {$file}" . PHP_EOL;
    }
}

function codeSnifferDiff(array $changedFiles): array
{
    $ret = [];
    foreach ($changedFiles as $file => $status) {
        $diff = shell_exec("/Users/ha/Code/bin/php-cs-fixer/vendor/bin/php-cs-fixer check --diff --using-cache no --rules @PSR12 {$file} 2>/dev/null");

        if (is_null($diff)) {
            continue;
        }

        if (!str_contains($diff, 'Found 1 of 1 files')) {
            continue;
        }

        $ret[] = "{$file}";
        $ret[] = str_replace(PHP_EOL, PHP_EOL . " > ", $diff);
    }

    return $ret;
}

function grep(array $changedFiles, $for): array
{
    $ret = [];
    foreach ($changedFiles as $file => $status) {
        $grepped = shell_exec("grep -i '{$for}' {$file}");

        if (is_null($grepped)) {
            continue;
        }

        $grepped = explode(PHP_EOL, $grepped);

        if (!sizeof($grepped)) {
            continue;
        }

        $ret[] = "{$file}";
        foreach ($grepped as $grp) {
            $grp = trim($grp);

            if (empty($grp)) {
                continue;
            }

            $ret[] = " -> $grp";
        }
    }

    return $ret;
}

function checkFilesNotEndingInNewLine(array $changedFiles): array
{
    $ret = [];
    foreach ($changedFiles as $file => $status) {
        $lastLine = shell_exec("tail -c 1 {$file}");
        if ($lastLine !== "\n") {
            $ret[] = $file;
        }
    }

    return $ret;
}

function getChangedFiles(Choice $choice): array
{
    $mainBranch = $choice->main;
    $command = "git diff --name-status {$mainBranch}";

    $result = shell_exec($command);

    if (empty($result)) {
        return [];
    }

    $result = trim($result);

    $statusFiles = explode(PHP_EOL, $result);

    $ret = [];
    foreach ($statusFiles as $sf) {
        $fields = preg_split('#\s+#', $sf);
        $status = handleStatus(array_shift($fields));

        if ($status === 'D') { // deleted
            continue;
        }

        $file = handleRenameStatusIfNecessary($status, $fields);

        $ret[$file] = $status;
    }

    return $ret;
}

function handleStatus(string $status): string
{
    if (!str_starts_with($status, 'R')) {
        return trim($status);
    }

    return 'R';
}

function handleRenameStatusIfNecessary(string $status, array $fields): string
{
    if ($status !== 'R') {
        return trim($fields[0]);
    }

    $newFile = array_pop($fields);

    return trim($newFile);
}

class Choice
{
    public function __construct(
        public readonly string $name,
        public readonly string $path,
        public readonly string $main
    ){}
}