OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

TemplateClosureTest.php

5 KB
<?php

declare(strict_types=1);

/**
 * Deckt ab, dass keine Vorlage aus einer statischen Funktion heraus auf die
 * Vorlagen-Helfer zugreift.
 *
 * Eine mit `static` deklarierte Funktion hat kein `$this`. Der Zugriff fällt
 * erst beim Ausliefern der Seite auf – und bricht sie dann ganz ab.
 */
return static function (): void {

    /** Alle Vorlagen, Pfad relativ zum Projekt. */
    $dateien = static function (): array {
        $wurzel = dirname(__DIR__) . '/templates';
        $alle   = [];
        $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($wurzel));
        foreach ($it as $datei) {
            if ($datei->isFile() && $datei->getExtension() === 'php') {
                $alle[] = $datei->getPathname();
            }
        }
        sort($alle);
        return $alle;
    };

    /**
     * Sucht statische Funktionen, deren Rumpf `$this` verwendet.
     *
     * Gearbeitet wird auf den Token des Quelltexts: Nur so lassen sich die
     * Grenzen einer Funktion sicher bestimmen – ein Suchmuster über den Text
     * greift entweder zu weit oder zu kurz.
     *
     * @return list<int> Zeilennummern der Fundstellen.
     */
    $fundstellen = static function (string $quelle): array {
        $token = token_get_all($quelle);
        $treffer = [];
        $anzahl  = count($token);

        for ($i = 0; $i < $anzahl; $i++) {
            if (!is_array($token[$i]) || $token[$i][0] !== T_STATIC) {
                continue;
            }
            // Nächstes bedeutungstragendes Token: function oder fn?
            $j = $i + 1;
            while ($j < $anzahl && is_array($token[$j]) && in_array($token[$j][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) {
                $j++;
            }
            if (!is_array($token[$j] ?? null) || !in_array($token[$j][0], [T_FUNCTION, T_FN], true)) {
                continue;
            }
            $istPfeil = $token[$j][0] === T_FN;
            $zeile    = $token[$j][2];

            // Rumpf abgrenzen: beim Pfeil bis zum Ende des Ausdrucks,
            // sonst über die geschweiften Klammern.
            $tiefe = 0;
            $start = $j;
            $ende  = $anzahl - 1;
            $imRumpf = false;
            for ($k = $j; $k < $anzahl; $k++) {
                $t = $token[$k];
                $text = is_array($t) ? $t[1] : $t;
                if (in_array($text, ['(', '[', '{'], true)) {
                    $tiefe++;
                } elseif (in_array($text, [')', ']', '}'], true)) {
                    $tiefe--;
                    if (!$istPfeil && $imRumpf && $tiefe === 0) {
                        $ende = $k;
                        break;
                    }
                } elseif ($text === '{' || (is_array($t) && $t[0] === T_CURLY_OPEN)) {
                    $imRumpf = true;
                }
                if (!$istPfeil && $text === '{' && !$imRumpf) {
                    $imRumpf = true;
                }
                if ($istPfeil && $tiefe <= 0 && in_array($text, [';', ','], true) && $k > $j + 2) {
                    $ende = $k;
                    break;
                }
            }

            for ($k = $start; $k <= $ende; $k++) {
                $t = $token[$k];
                if (is_array($t) && $t[0] === T_VARIABLE && $t[1] === '$this') {
                    $treffer[] = $zeile;
                    break;
                }
            }
        }
        return $treffer;
    };

    test('Keine statische Funktion einer Vorlage greift auf $this zu', function () use ($dateien, $fundstellen) {
        $wurzel = dirname(__DIR__) . '/';
        $fehler = [];
        foreach ($dateien() as $datei) {
            foreach ($fundstellen((string)file_get_contents($datei)) as $zeile) {
                $fehler[] = str_replace($wurzel, '', $datei) . ':' . $zeile;
            }
        }
        assertSame([], $fehler, 'Zur Laufzeit ohne $this: ' . implode(', ', $fehler));
    });

    test('Der Suchlauf erkennt eine statische Funktion mit $this', function () use ($fundstellen) {
        // Sonst wäre der Test oben immer grün.
        assertSame([2], $fundstellen("<?php\n\$f = static fn(\$v) => \$this->money(\$v);\n"));
        assertSame([2], $fundstellen("<?php\n\$f = static function (\$v) {\n    return \$this->money(\$v);\n};\n"));
        assertSame([], $fundstellen("<?php\n\$f = fn(\$v) => \$this->money(\$v);\n"));
        assertSame([], $fundstellen("<?php\n\$f = static fn(\$v) => \$v * 2;\n\$g = \$this->money(1);\n"));
    });

    test('Der Suchlauf erfasst ueberhaupt Vorlagen', function () use ($dateien) {
        assertTrue(count($dateien()) > 200, 'Die Vorlagen konnten nicht gelesen werden');
    });
};