/ devlog / devscripts / check_pure_logic.sh
#!/bin/sh
# Names each method a patch adds that could be checked on its own, and
# says whether a test file changed in the same patch.
#
#   devlog/devscripts/check_pure_logic.sh
#
# WHY THIS EXISTS. The process file asks that pure logic be pulled out
# so it can be checked on its own. Twice I wrote in a delivery that a
# new method was one input and one output, and wrote no case for it:
# emojiGroups, and the method building an ffmpeg option string. Noticing
# is not the hard part; noticing at the moment the patch is cut is. This
# looks for a method a patch adds whose body reads no property, calls
# nothing on the object, and touches no file or database, which is the
# shape a case can stand behind with no test set-up at all.
#
# It warns rather than refuses, since such a method may be covered by a
# case on the method that calls it.
set -u
WORK=${WORK:-/home/claude/work}
cd "$WORK" || exit 2
python3 - <<'PYEOF'
import re
import subprocess

changed = subprocess.run(["git", "diff", "confirmed-head..HEAD",
    "--name-only"], capture_output=True, text=True).stdout.split()
tests_touched = [one for one in changed if one.startswith("tests/")]
reaches_out = re.compile(r"(file_|fopen|curl_|->db|->query|exec\(|"
    r"unlink|mkdir|require|tl\()")
found = []
for path in changed:
    if not path.startswith("src/") or not path.endswith(".php"):
        continue
    was = subprocess.run(["git", "show", "confirmed-head:" + path],
        capture_output=True, text=True)
    older = set()
    if was.returncode == 0:
        older = set(re.findall(r"function\s+(\w+)", was.stdout))
    try:
        text = open(path, encoding="utf-8").read()
    except OSError:
        continue
    for m in re.finditer(r"function\s+(\w+)\s*\([^)]*\)[^{]*\{", text):
        name = m.group(1)
        if name in older:
            continue
        start = m.end() - 1
        depth = 0
        at = start
        while at < len(text):
            if text[at] == "{":
                depth += 1
            elif text[at] == "}":
                depth -= 1
                if depth == 0:
                    break
            at += 1
        body = text[start:at]
        if "$this" in body or "::" in body:
            continue
        if reaches_out.search(body):
            continue
        if len(body.split("\n")) < 4:
            continue
        found.append((path, name))
if not found:
    print("check: the patch adds no method that stands on its own")
else:
    for path, name in found:
        print("%s: %s reads nothing outside itself" % (path, name))
    if tests_touched:
        print("check: %d such method(s); the patch also changes %s" %
            (len(found), ", ".join(tests_touched)))
    else:
        print("check: %d such method(s) and no test file changed. Each "
            "could carry a case." % len(found))
PYEOF
X