/ devlog / devscripts / check_cross_class_calls.php
<?php
/* A view that calls another element's method dies where it calls when
   that method is private or protected. PHP only complains when the line
   runs, so a branch nobody has opened lately hides it: setting a wiki
   page to the Git repository kind crashed that way, on a method the wiki
   element had been calling all along.

   This reads every call written as element("name")->method( or
   helper("name")->method( and checks how that method is declared in the
   matching element or helper class. A method declared private or
   protected but called from another class is named.

   php devlog/devscripts/check_cross_class_calls.php
*/
$kinds = ["element" => "src/views/elements", "helper" => "src/views/helpers"];
$declared = [];
foreach ($kinds as $kind => $folder) {
    foreach (glob($folder . "/*.php") as $path) {
        $class = basename($path, ".php");
        $lines = file($path);
        foreach ($lines as $one) {
            if (preg_match('/^\s*(public|private|protected)\s+' .
                '(?:static\s+)?function\s+(\w+)/', $one, $found)) {
                $declared[$kind][strtolower($class)][$found[2]] =
                    $found[1];
            }
        }
    }
}
$hidden = [];
$walk = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator("src"));
foreach ($walk as $file) {
    if ($file->isDir() || substr($file->getFilename(), -4) !== ".php") {
        continue;
    }
    $path = $file->getPathname();
    $caller = basename($path, ".php");
    $lines = file($path);
    foreach ($lines as $at => $one) {
        if (!preg_match_all('/(element|helper)\(\s*["\'](\w+)["\']\s*\)' .
            '\s*->\s*(\w+)\s*\(/', $one, $found, PREG_SET_ORDER)) {
            continue;
        }
        foreach ($found as $call) {
            $kind = $call[1];
            $named = strtolower($call[2]) .
                (($kind == "element") ? "element" : "helper");
            $method = $call[3];
            /* A class calling its own method is free to reach a method
               kept to itself, so only a call from elsewhere counts. */
            if (strtolower($caller) === $named) {
                continue;
            }
            $how = $declared[$kind][$named][$method] ?? "";
            if ($how === "private" || $how === "protected") {
                $hidden[] = basename($path) . ":" . ($at + 1) .
                    " calls " . $call[2] . " " . $kind . "'s " .
                    $method . ", which is " . $how;
            }
        }
    }
}
$hidden = array_unique($hidden);
foreach ($hidden as $one) {
    echo "  " . $one . "\n";
}
echo "check: " . (count($hidden) ?
    count($hidden) . " call(s) reach a method another class keeps to itself" :
    "every cross-class call reaches a method open to it") . "\n";
exit(count($hidden) ? 1 : 0);
X