/ devlog / devscripts / check_parent_vars.php
<?php
/* A component method that reaches through $parent without setting it
   first dies where it reaches. The component holds the controller as
   $this->parent, and the convention in these files is to take a copy
   at the top of a method; a method that skips that line and uses the
   name anyway looks right and crashes when it runs.

   This has now been reported four times, each from a different screen:
   the podcast download job, the group controller, the mail attachment
   read, and making a wiki page an article page. Each was one missing
   line. A fifth was sitting unreported on the payment error path.

   This reads every method in the tree for a $parent used where nothing
   in that method set it. A method handed $parent as an argument, or one
   that sets it from anything at all, counts as having it.

   php devlog/devscripts/check_parent_vars.php
*/
$paths = [];
foreach (["src", "tests"] as $folder) {
    $walk = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($folder));
    foreach ($walk as $file) {
        if ($file->isDir() || substr($file->getFilename(), -4) !== ".php") {
            continue;
        }
        $paths[] = $file->getPathname();
    }
}
$missing = [];
foreach ($paths as $path) {
    $lines = file($path);
    $starts = [];
    foreach ($lines as $at => $one) {
        if (preg_match('/^\s*(?:public|private|protected|static|\s)*' .
            'function\s+(\w+)/', $one, $found)) {
            $starts[] = [$at, $found[1]];
        }
    }
    $starts[] = [count($lines), ""];
    for ($which = 0; $which < count($starts) - 1; $which++) {
        $from = $starts[$which][0];
        $to = $starts[$which + 1][0];
        $body = implode("", array_slice($lines, $from, $to - $from));
        /* A docblock before the next method is not part of this one,
           and reading it counted names that method is handed. */
        $doc_at = strpos($body, "\n    /**");
        if ($doc_at !== false) {
            $body = substr($body, 0, $doc_at);
        }
        /* Only a reach through the name counts as a use: a bare mention
           in a comment or a string does not run. */
        if (!preg_match('/\$parent(->|\[)/', $body)) {
            continue;
        }
        /* Given as an argument, or given a value anywhere. */
        if (preg_match('/function\s+\w+\s*\([^)]*\$parent\b/s', $body) ||
            preg_match('/\$parent\s*=[^=]/', $body)) {
            continue;
        }
        $missing[] = basename($path) . "::" . $starts[$which][1] .
            " reaches through \$parent";
    }
}
foreach ($missing as $one) {
    echo "  " . $one . "\n";
}
echo "check: " . (count($missing) ?
    count($missing) . " method(s) reach through a parent nothing set" :
    "every parent a method reaches through is set there") . "\n";
exit(count($missing) ? 1 : 0);
X