OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

CronTaskContextTest.php

5 KB
<?php

declare(strict_types=1);

/**
 * Statische Prüfung der Cron-Handler: Der TaskContext wird ausschließlich als
 * Parameter durchgereicht (es gibt keine Handler-Eigenschaft dafür). Eine
 * Hilfsmethode, die `$ctx` benutzt, muss ihn deshalb selbst entgegennehmen –
 * sonst ist die Variable im Methodenrumpf schlicht ungebunden und der Aufruf
 * bricht erst zur Laufzeit ab, wenn eine Mail eintrifft.
 */
return static function (): void {

    /**
     * Sucht Methoden, die `$ctx` lesen, ohne ihn als Parameter zu deklarieren.
     *
     * Closures und Pfeilfunktionen bekommen keinen eigenen Geltungsbereich:
     * Ihre Parameter und `use`-Übernahmen gelten als gebunden, damit übernommene
     * Variablen nicht fälschlich als ungebunden gemeldet werden.
     *
     * @return string[] Fundstellen als "datei:zeile methode()"
     */
    $scan = static function (string $dir): array {
        $findings = [];
        $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir));
        foreach ($files as $file) {
            if ($file->getExtension() !== 'php') {
                continue;
            }
            $tokens = token_get_all(file_get_contents($file->getPathname()));
            $count  = count($tokens);
            $method = null;
            $bound  = [];

            for ($i = 0; $i < $count; $i++) {
                $tok = $tokens[$i];

                if (is_array($tok) && $tok[0] === T_FUNCTION) {
                    // Nächstes bedeutsames Token entscheidet: benannte Methode oder Closure
                    $j = $i + 1;
                    while ($j < $count && is_array($tokens[$j]) && in_array($tokens[$j][0], [T_WHITESPACE, T_COMMENT], true)) {
                        $j++;
                    }
                    $named = is_array($tokens[$j]) && $tokens[$j][0] === T_STRING;

                    $open = $j;
                    while ($open < $count && $tokens[$open] !== '(') {
                        $open++;
                    }
                    $params = [];
                    $depth  = 0;
                    for ($k = $open; $k < $count; $k++) {
                        if ($tokens[$k] === '(') {
                            $depth++;
                        } elseif ($tokens[$k] === ')') {
                            if (--$depth === 0) {
                                break;
                            }
                        } elseif (is_array($tokens[$k]) && $tokens[$k][0] === T_VARIABLE) {
                            $params[$tokens[$k][1]] = true;
                        }
                    }
                    // use (...) einer Closure hängt direkt hinter der Parameterliste
                    $u = $k + 1;
                    while ($u < $count && is_array($tokens[$u]) && in_array($tokens[$u][0], [T_WHITESPACE, T_COMMENT], true)) {
                        $u++;
                    }
                    if (is_array($tokens[$u]) && $tokens[$u][0] === T_USE) {
                        $depth = 0;
                        for ($k = $u; $k < $count; $k++) {
                            if ($tokens[$k] === '(') {
                                $depth++;
                            } elseif ($tokens[$k] === ')') {
                                if (--$depth === 0) {
                                    break;
                                }
                            } elseif (is_array($tokens[$k]) && $tokens[$k][0] === T_VARIABLE) {
                                $params[$tokens[$k][1]] = true;
                            }
                        }
                    }

                    if ($named) {
                        $method = $tokens[$j][1];
                        $bound  = $params;
                    } else {
                        $bound += $params;
                    }
                    $i = $k;
                    continue;
                }

                if (is_array($tok) && $tok[0] === T_VARIABLE && $tok[1] === '$ctx'
                    && $method !== null && !isset($bound['$ctx'])) {
                    $findings[] = $file->getPathname() . ':' . $tok[2] . ' ' . $method . '()';
                }
            }
        }
        sort($findings);
        return $findings;
    };

    test('Cron-Handler reichen den TaskContext als Parameter durch', function () use ($scan) {
        $findings = $scan(dirname(__DIR__) . '/src/Cron');
        assertSame([], $findings, 'Ungebundenes $ctx: ' . implode(', ', $findings));
    });

    test('Die Prüfung erkennt eine Methode ohne $ctx-Parameter', function () use ($scan) {
        $tmp = sys_get_temp_dir() . '/opennit-ctx-' . getmypid();
        @mkdir($tmp, 0700, true);
        file_put_contents($tmp . '/Probe.php', <<<'PHP'
        <?php
        final class Probe
        {
            public function execute($ctx): void { $this->helfer($ctx); }
            private function helfer($manager): void { $this->wert($ctx); }
        }
        PHP);

        $findings = $scan($tmp);
        assertSame(1, count($findings));
        assertTrue(str_contains($findings[0], 'helfer()'));

        unlink($tmp . '/Probe.php');
        rmdir($tmp);
    });
};