#!/usr/bin/env python3 """state-key-audit.py — the analyzer behind scripts/verify-state-keys.sh. Read that script's header for WHY this exists (issue #129). This file is the HOW: a small El reader that resolves the key expression at every state_get / state_set site, including keys that are computed. WHAT IT PARSES El as this engine writes it: `fn f(a: T, b: T) -> T { ... }`, `let x: T = e`, `return e`, `if c { a } else { b }` as an expression, `+` concatenation, `"..."` with backslash escapes, `//` line comments. No block comments, no const/match/struct exist in this dialect (verified over the whole tree). KEY PATTERNS — the only two things a key expression can resolve to EXACT "soul_model" the whole key is known PREFIX "session_hist_" a known head, then runtime text (plus UNRESOLVED, which is a report line and never a failure) RESOLUTION — resolve_expr() returns a SET of patterns; unions are how branches, multiple returns, and multiple bindings of one name are represented. literal "k" -> {EXACT k} concat A + B -> fold left; all-static -> EXACT, static head + dynamic tail -> PREFIX if-expression if c {A} else {B} -> resolve(A) | resolve(B), except that str_eq(X,"") with X statically "" folds to the taken branch only call f(args) -> union over f's return expressions, with f's params bound to THIS call site's actual argument expressions local var let k = e; state_get(k)-> union over every `let k =` in the enclosing function parameter fn g(k) { state_get(k) }-> union over the argument at that position across every call site of g anything else json_get(...), env(...)-> UNRESOLVED Recursion is depth- and cycle-guarded; a guard trip yields UNRESOLVED, never a failure. COVERAGE — a read is satisfied when some write can produce the same key: read EXACT k <- write EXACT k, or write PREFIX p where k starts with p read PREFIX p <- write EXACT k where k starts with p, or write PREFIX q where p and q are prefixes of each other Deliberately permissive at the boundaries: a gate that cries wolf gets deleted. """ import os import re import sys MAX_DEPTH = 12 # ── patterns ──────────────────────────────────────────────────────────────── EXACT = "exact" PREFIX = "prefix" def pat_exact(s): return (EXACT, s) def pat_prefix(s): # A prefix with no static text at all carries no information; that is the # UNRESOLVED case, not a pattern. return (PREFIX, s) if s else None def covers(write, read): """Can a write of pattern `write` produce a key that `read` reads? The prefix rule is DIRECTIONAL, and that direction is the whole point. A write namespace that is the same or BROADER than the read namespace covers it (write "rl:" covers read "rl:x"). A write namespace that is NARROWER does NOT (write "session_histv2_" does not cover read "session_hist_") — being permissive there re-opens the exact hole this gate exists to close: rename the producer, leave the readers, stay green. Verified with a control run that renames sessions.el's writer and leaves its four readers behind.""" wk, wv = write rk, rv = read if rk == EXACT: return rv == wv if wk == EXACT else rv.startswith(wv) # read is a PREFIX: some key starting with rv is read if wk == EXACT: return wv.startswith(rv) # that one written key is in range return rv.startswith(wv) # write namespace same-or-broader # ── lexer ─────────────────────────────────────────────────────────────────── TOK_STR, TOK_IDENT, TOK_PUNCT, TOK_NUM = "str", "ident", "punct", "num" IDENT_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") NUM_RE = re.compile(r"[0-9]+(\.[0-9]+)?") class Tok: __slots__ = ("kind", "val", "line") def __init__(self, kind, val, line): self.kind, self.val, self.line = kind, val, line def __repr__(self): return "%s(%r)@%d" % (self.kind, self.val, self.line) def lex(src): toks, i, n, line = [], 0, len(src), 1 while i < n: c = src[i] if c == "\n": line += 1 i += 1 continue if c in " \t\r": i += 1 continue if c == "/" and i + 1 < n and src[i + 1] == "/": while i < n and src[i] != "\n": i += 1 continue if c == '"': j, buf = i + 1, [] while j < n: if src[j] == "\\" and j + 1 < n: esc = src[j + 1] buf.append({"n": "\n", "t": "\t", "r": "\r"}.get(esc, esc)) j += 2 continue if src[j] == '"': break if src[j] == "\n": line += 1 buf.append(src[j]) j += 1 toks.append(Tok(TOK_STR, "".join(buf), line)) i = j + 1 continue m = IDENT_RE.match(src, i) if m: toks.append(Tok(TOK_IDENT, m.group(0), line)) i = m.end() continue m = NUM_RE.match(src, i) if m: toks.append(Tok(TOK_NUM, m.group(0), line)) i = m.end() continue toks.append(Tok(TOK_PUNCT, c, line)) i += 1 return toks def match_close(toks, i, open_ch, close_ch): """toks[i] is open_ch; return index of its matching close_ch.""" depth = 0 while i < len(toks): if toks[i].kind == TOK_PUNCT: if toks[i].val == open_ch: depth += 1 elif toks[i].val == close_ch: depth -= 1 if depth == 0: return i i += 1 return len(toks) - 1 # ── program model ─────────────────────────────────────────────────────────── class Func: def __init__(self, name, path, line, params, toks, start, end): self.name, self.path, self.line = name, path, line self.params = params # [param name] self.toks = toks # the whole file's token list self.start, self.end = start, end # body token range, exclusive of braces self.lets = None # name -> [expr token ranges], lazily built class Site: def __init__(self, kind, path, line, func, arg_range, text): self.kind = kind # "get" | "set" self.path, self.line = path, line self.func = func self.arg_range = arg_range self.text = text # source text of the key expression self.pats = set() self.unresolved = False self.literal = None # set when the key expression is a bare literal class Program: def __init__(self): self.files = {} # path -> toks self.funcs = {} # name -> [Func] (El allows no overloads, but be safe) self.toplevel = [] # [Func] one per file, params=[] self.sites = [] # [Site] self.calls = {} # callee name -> [(Func caller, [arg ranges])] # -- loading ------------------------------------------------------------ def load(self, path, rel): with open(path, "r", encoding="utf-8", errors="replace") as fh: src = fh.read() toks = lex(src) self.files[rel] = toks self._scan_funcs(rel, toks) def _scan_funcs(self, rel, toks): covered = [] i = 0 while i < len(toks): t = toks[i] if t.kind == TOK_IDENT and t.val == "fn" and i + 2 < len(toks) \ and toks[i + 1].kind == TOK_IDENT and toks[i + 2].val == "(": name = toks[i + 1].val pclose = match_close(toks, i + 2, "(", ")") params = self._params(toks, i + 3, pclose) bopen = pclose + 1 while bopen < len(toks) and toks[bopen].val != "{": bopen += 1 bclose = match_close(toks, bopen, "{", "}") f = Func(name, rel, t.line, params, toks, bopen + 1, bclose) self.funcs.setdefault(name, []).append(f) covered.append((i, bclose)) i = bclose + 1 continue i += 1 # everything outside a fn is the file's top-level "function" tl = Func("" % rel, rel, 1, [], toks, 0, len(toks)) tl.covered = covered self.toplevel.append(tl) @staticmethod def _params(toks, i, end): """`a: T, b: T` -> ['a','b'] (top-level commas only).""" names, depth, expect = [], 0, True while i < end: t = toks[i] if t.kind == TOK_PUNCT and t.val in "([{": depth += 1 elif t.kind == TOK_PUNCT and t.val in ")]}": depth -= 1 elif depth == 0 and t.kind == TOK_PUNCT and t.val == ",": expect = True elif depth == 0 and expect and t.kind == TOK_IDENT: names.append(t.val) expect = False i += 1 return names def func_at(self, rel, tok_index): for f in self.funcs_in(rel): if f.start <= tok_index < f.end: return f for f in self.toplevel: if f.path == rel: return f return None def funcs_in(self, rel): for fl in self.funcs.values(): for f in fl: if f.path == rel: yield f # -- indexing ----------------------------------------------------------- def index(self): for rel, toks in self.files.items(): i = 0 while i < len(toks): t = toks[i] if t.kind == TOK_IDENT and i + 1 < len(toks) and toks[i + 1].val == "(" \ and t.val not in KEYWORDS \ and not (i > 0 and toks[i - 1].kind == TOK_IDENT and toks[i - 1].val == "fn"): # ^ the `fn f(a: T)` declaration is not a call site; counting # it as one makes every parameter resolve to its own name # and reports the whole function UNRESOLVED. close = match_close(toks, i + 1, "(", ")") args = split_args(toks, i + 2, close) self.calls.setdefault(t.val, []).append( (self.func_at(rel, i), args, rel, t.line)) if t.val in ("state_get", "state_set") and args: self.sites.append(Site( "get" if t.val == "state_get" else "set", rel, t.line, self.func_at(rel, i), args[0], render(toks, *args[0]))) i += 1 # -- resolution --------------------------------------------------------- def lets_of(self, f): if f.lets is not None: return f.lets f.lets = {} toks = f.toks skip = getattr(f, "covered", []) i = f.start while i < f.end: if any(a <= i <= b for a, b in skip): i = max(b for a, b in skip if a <= i <= b) + 1 continue t = toks[i] if t.kind == TOK_IDENT and t.val == "let" and i + 1 < f.end \ and toks[i + 1].kind == TOK_IDENT: name = toks[i + 1].val j = i + 2 if j < f.end and toks[j].val == ":": # skip the type while j < f.end and toks[j].val != "=": j += 1 if j < f.end and toks[j].val == "=": s = j + 1 e = stmt_end(toks, s, f.end) f.lets.setdefault(name, []).append((s, e)) i = e continue i += 1 return f.lets def returns_of(self, ctx, depth=0, seen=None): """The value expressions of a function, in the context it was CALLED in. Context-sensitive on purpose. `conv_hist_key` is written as a guard: if str_eq(session_id, "") { return "conv_history" } return "session_hist_" + session_id Collecting both returns flat would make state_set(conv_hist_key("")) — the dead handle_chat() write — claim to produce the session_hist_ namespace too. That is a producer this engine does not actually have, and claiming it would let the gate stay green if sessions.el's real writer vanished: a masking hole in the exact namespace #129 lives in. So a guard whose condition folds is honoured, and the branch not taken is dropped.""" out = [] self._values(ctx.toks, ctx.start, ctx.end, ctx, depth, seen if seen is not None else set(), out) return out def _values(self, toks, s, e, ctx, depth, seen, out): """Append the value expressions of a statement sequence. Returns True when the sequence definitely returns (rest unreachable).""" if depth > MAX_DEPTH: return False i = s while i < e: t = toks[i] if t.kind == TOK_IDENT and t.val == "return": j = stmt_end(toks, i + 1, e) if j > i + 1: out.append((i + 1, j)) return True if t.kind == TOK_IDENT and t.val == "let": i = stmt_end(toks, i + 2, e) continue if t.kind == TOK_IDENT and t.val == "if": i = self._if_stmt(toks, i, e, ctx, depth, seen, out) if i is True: return True continue if t.kind == TOK_PUNCT and t.val in "([{": i = match_close(toks, i, t.val, {"(": ")", "[": "]", "{": "}"}[t.val]) + 1 continue en = stmt_end(toks, i, e) if en <= i: i += 1 continue if en >= e: # trailing expression = the value out.append((i, en)) i = en return False def _if_stmt(self, toks, i, e, ctx, depth, seen, out): """Walk one if / else-if / else chain. Returns the next index, or True if the chain definitely returns on every reachable branch.""" bopen = i + 1 while bopen < e and toks[bopen].val != "{": bopen += 1 if bopen >= e: return e bclose = match_close(toks, bopen, "{", "}") fold = self._fold_cond(toks, i + 1, bopen, ctx, depth, seen) j = bclose + 1 else_s = else_e = None if j < e and toks[j].kind == TOK_IDENT and toks[j].val == "else": if j + 1 < e and toks[j + 1].val == "{": ec = match_close(toks, j + 1, "{", "}") else_s, else_e = j + 2, ec j = ec + 1 else: # `else if ...` — the rest of the chain else_s = j + 1 else_e = stmt_end(toks, j + 1, e) j = else_e then_ret = else_ret = False if fold is not False: then_ret = self._values(toks, bopen + 1, bclose, ctx, depth + 1, seen, out) if fold is not True and else_s is not None: else_ret = self._values(toks, else_s, else_e, ctx, depth + 1, seen, out) if fold is True and then_ret: return True if fold is False and else_s is not None and else_ret: return True if fold is None and else_s is not None and then_ret and else_ret: return True return j def resolve(self, rng, func, depth=0, seen=None): """-> (set of patterns, unresolved_flag)""" if seen is None: seen = set() if depth > MAX_DEPTH: return set(), True return self._expr(func.toks, rng[0], rng[1], func, depth, seen) # -- expression walker -------------------------------------------------- def _expr(self, toks, s, e, func, depth, seen): parts, cur, d = [], s, 0 i = s while i < e: # split on top-level '+' v = toks[i].val if toks[i].kind == TOK_PUNCT and v in "([{": d += 1 elif toks[i].kind == TOK_PUNCT and v in ")]}": d -= 1 elif d == 0 and toks[i].kind == TOK_PUNCT and v == "+" and i > s: parts.append((cur, i)) cur = i + 1 i += 1 parts.append((cur, e)) if len(parts) == 1: return self._primary(toks, s, e, func, depth, seen) # concatenation: keep folding while every operand so far is EXACT head, unres = "", False static = True for (ps, pe) in parts: pats, u = self._primary(toks, ps, pe, func, depth, seen) exacts = {p[1] for p in pats if p[0] == EXACT} if static and len(exacts) == 1 and not u and len(pats) == 1: head += exacts.pop() continue if static and pats and all(p[0] == EXACT for p in pats) and len(pats) > 1: # a branchy static operand: keep the shared head only static = False head += os.path.commonprefix(sorted({p[1] for p in pats})) break static = False # first non-static operand: everything after it is runtime text if (ps, pe) == parts[0]: for p in pats: if p[0] == PREFIX: head = p[1] break if not head: unres = True break if static: return {pat_exact(head)}, False p = pat_prefix(head) return ({p} if p else set()), (unres or not p) def _primary(self, toks, s, e, func, depth, seen): while s < e and toks[s].kind == TOK_PUNCT and toks[s].val == "(" \ and match_close(toks, s, "(", ")") == e - 1: s, e = s + 1, e - 1 if s >= e: return set(), True t = toks[s] if t.kind == TOK_STR and e == s + 1: return {pat_exact(t.val)}, False if t.kind == TOK_IDENT and t.val == "if": return self._if_expr(toks, s, e, func, depth, seen) if t.kind == TOK_IDENT and s + 1 < e and toks[s + 1].val == "(": close = match_close(toks, s + 1, "(", ")") if close == e - 1: return self._call(toks, t.val, split_args(toks, s + 2, close), func, depth, seen) if t.kind == TOK_IDENT and e == s + 1: return self._var(t.val, func, depth, seen) return set(), True def _if_expr(self, toks, s, e, func, depth, seen): bopen = s + 1 while bopen < e and toks[bopen].val != "{": bopen += 1 cond = (s + 1, bopen) bclose = match_close(toks, bopen, "{", "}") then_rng = block_tail(toks, bopen + 1, bclose) or (bopen + 1, bclose) else_rng = None j = bclose + 1 if j < e and toks[j].kind == TOK_IDENT and toks[j].val == "else": if j + 1 < e and toks[j + 1].val == "{": ec = match_close(toks, j + 1, "{", "}") else_rng = block_tail(toks, j + 2, ec) or (j + 2, ec) else: else_rng = (j + 1, e) # `else if ...` taken = self._fold_cond(toks, cond[0], cond[1], func, depth, seen) rngs = [] if taken is not False: rngs.append(then_rng) if taken is not True and else_rng: rngs.append(else_rng) pats, unres = set(), False for r in rngs: p, u = self._expr(toks, r[0], r[1], func, depth + 1, seen) pats |= p unres = unres or u return pats, unres def _fold_cond(self, toks, s, e, func, depth, seen): """Constant-fold `str_eq(X, "")` / `!str_eq(X, "")` so a helper called with a literal (conv_hist_key("")) yields only the branch it really takes. Returns True / False / None(unknown).""" neg = False if s < e and toks[s].kind == TOK_PUNCT and toks[s].val == "!": neg, s = True, s + 1 if not (s < e and toks[s].kind == TOK_IDENT and toks[s].val == "str_eq" and s + 1 < e and toks[s + 1].val == "("): return None close = match_close(toks, s + 1, "(", ")") if close != e - 1: return None args = split_args(toks, s + 2, close) if len(args) != 2: return None va, ua = self._expr(toks, args[0][0], args[0][1], func, depth + 1, seen) vb, ub = self._expr(toks, args[1][0], args[1][1], func, depth + 1, seen) if ua or ub or len(va) != 1 or len(vb) != 1: return None (ka, sa), (kb, sb) = va.pop(), vb.pop() if ka != EXACT or kb != EXACT: return None r = (sa == sb) return (not r) if neg else r def _call(self, toks, name, args, func, depth, seen): cands = self.funcs.get(name) if not cands: return set(), True # builtin: json_get, env, ... pats, unres = set(), False for callee in cands: key = ("fn", callee.path, callee.name, tuple(args)) if key in seen: unres = True continue seen = seen | {key} # bind the callee's params to THIS call site's argument expressions binding = {} for idx, pname in enumerate(callee.params): if idx < len(args): binding[pname] = (args[idx], func) callee_ctx = _Bound(callee, binding) for r in self.returns_of(callee_ctx, depth + 1, seen): p, u = self._expr(callee.toks, r[0], r[1], callee_ctx, depth + 1, seen) pats |= p unres = unres or u return pats, unres def _var(self, name, func, depth, seen): real = func.func if isinstance(func, _Bound) else func # 1. a parameter bound by the call site we came through if isinstance(func, _Bound) and name in func.binding: rng, caller_ctx = func.binding[name] return self._expr(caller_ctx.toks, rng[0], rng[1], caller_ctx, depth + 1, seen) # 2. a local `let` in the enclosing function lets = self.lets_of(real) if name in lets: key = ("let", real.path, real.name, name) if key in seen: return set(), True seen = seen | {key} pats, unres = set(), False for rng in lets[name]: p, u = self._expr(real.toks, rng[0], rng[1], real, depth + 1, seen) pats |= p unres = unres or u return pats, unres # 3. an unbound parameter -> look at every call site of the enclosing fn if name in real.params: key = ("param", real.path, real.name, name) if key in seen: return set(), True seen = seen | {key} idx = real.params.index(name) pats, unres = set(), False sites = self.calls.get(real.name, []) if not sites: return set(), True for caller, args, _rel, _line in sites: if caller is None or idx >= len(args): unres = True continue p, u = self._expr(caller.toks, args[idx][0], args[idx][1], caller, depth + 1, seen) pats |= p unres = unres or u return pats, unres # 4. a file-level / cross-file top-level `let` for tl in self.toplevel: lets = self.lets_of(tl) if name in lets: key = ("let", tl.path, tl.name, name) if key in seen: return set(), True seen2 = seen | {key} pats, unres = set(), False for rng in lets[name]: p, u = self._expr(tl.toks, rng[0], rng[1], tl, depth + 1, seen2) pats |= p unres = unres or u return pats, unres return set(), True class _Bound: """A callee view that also knows what its params were called with.""" def __init__(self, func, binding): self.func, self.binding = func, binding self.toks, self.start, self.end = func.toks, func.start, func.end self.params, self.path, self.name = func.params, func.path, func.name def __getattr__(self, k): return getattr(self.func, k) # ── token helpers ─────────────────────────────────────────────────────────── def split_args(toks, s, e): out, cur, d = [], s, 0 i = s while i < e: v = toks[i].val if toks[i].kind == TOK_PUNCT and v in "([{": d += 1 elif toks[i].kind == TOK_PUNCT and v in ")]}": d -= 1 elif d == 0 and toks[i].kind == TOK_PUNCT and v == ",": out.append((cur, i)) cur = i + 1 i += 1 if cur < e: out.append((cur, e)) return out STMT_START = {"let", "return", "if", "while", "for"} KEYWORDS = {"if", "while", "for", "return", "fn", "let", "else", "match"} def stmt_end(toks, s, limit): """End of the expression starting at s: the next top-level statement boundary. El has no semicolons, so a newline that starts a new statement ends this one.""" d, i = 0, s while i < limit: t = toks[i] if t.kind == TOK_PUNCT and t.val in "([": d += 1 elif t.kind == TOK_PUNCT and t.val in ")]": d -= 1 if d < 0: return i elif t.kind == TOK_PUNCT and t.val == "{": # a brace at depth 0 belongs to this expression only when it is an # if/else block that is part of it d += 1 elif t.kind == TOK_PUNCT and t.val == "}": d -= 1 if d < 0: return i elif d == 0 and t.kind == TOK_PUNCT and t.val == ",": return i elif d == 0 and i > s and t.kind == TOK_IDENT and t.val in STMT_START: if t.val == "if" and toks[i - 1].kind == TOK_IDENT and toks[i - 1].val == "else": i += 1 continue return i elif d == 0 and i > s and t.kind == TOK_IDENT and t.val == "fn": return i i += 1 return limit def block_tail(toks, s, e): """The trailing expression of a block, if the block ends in one.""" i, last = s, None while i < e: t = toks[i] if t.kind == TOK_IDENT and t.val in ("let", "return"): i = stmt_end(toks, i + 1, e) last = None continue if t.kind == TOK_PUNCT and t.val in "([{": i = match_close(toks, i, t.val, {"(": ")", "[": "]", "{": "}"}[t.val]) + 1 continue st = i en = stmt_end(toks, i, e) if en <= st: i = st + 1 continue last = (st, en) i = en return last def render(toks, s, e): out = [] for t in toks[s:e]: out.append('"%s"' % t.val if t.kind == TOK_STR else t.val) return " ".join(out) # ── the gate ──────────────────────────────────────────────────────────────── def collect(root, include_tests): files = [] for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = [d for d in dirnames if d not in ("dist", "vendor", ".git", "node_modules")] rel_dir = os.path.relpath(dirpath, root) if not include_tests and rel_dir.split(os.sep)[0] == "tests": continue for fn in sorted(filenames): if fn.endswith(".el"): rel = os.path.normpath(os.path.join(rel_dir, fn)) files.append((os.path.join(dirpath, fn), rel)) return sorted(files, key=lambda x: x[1]) def is_bare_literal(prog, site): toks = prog.files[site.path] s, e = site.arg_range return e == s + 1 and toks[s].kind == TOK_STR def read_decl(path): """A declaration file: one entry per line, `# ...` comments stripped.""" out = [] if not path or not os.path.exists(path): return out with open(path) as fh: for ln in fh: ln = ln.split("#", 1)[0].strip() if ln: out.append(ln) return out def opt(argv, name, default=None): for i, a in enumerate(argv): if a == name and i + 1 < len(argv): return argv[i + 1] return default def main(argv): root = os.path.abspath(argv[1]) if len(argv) > 1 and not argv[1].startswith("-") else "." include_tests = "--include-tests" in argv verbose = "--verbose" in argv baseline_path = opt(argv, "--baseline") external_path = opt(argv, "--external") prog = Program() for path, rel in collect(root, include_tests): prog.load(path, rel) prog.index() for site in prog.sites: pats, unres = prog.resolve(site.arg_range, site.func) site.pats, site.unresolved = {p for p in pats if p}, unres if is_bare_literal(prog, site): site.literal = prog.files[site.path][site.arg_range[0]].val writes = [s for s in prog.sites if s.kind == "set"] reads = [s for s in prog.sites if s.kind == "get"] write_pats = set() for w in writes: write_pats |= w.pats # Declared host-set keys: written by something outside the El tree (an # operator, the installer, a host process). Each entry must carry a reason. external = [] for ln in read_decl(external_path): parts = ln.split(None, 1) if len(parts) != 2 or parts[0] not in (EXACT, PREFIX): print("bad --external line (want `exact|prefix `): %r" % ln, file=sys.stderr) return 2 external.append((parts[0], parts[1])) write_pats |= set(external) # F1 — a read of a key no write in the tree produces. f1 = [] for r in reads: for p in sorted(r.pats): if not any(covers(w, p) for w in write_pats): f1.append((r, p)) # F2 — a key namespace owned by a helper, accessed by a hand-rolled literal. # This is the #129 shape: the producer moved behind conv_hist_key() and # one consumer kept spelling the old key out by hand. owners = {} # helper fn name -> its value set for s in prog.sites: toks = prog.files[s.path] a, b = s.arg_range if toks[a].kind == TOK_IDENT and a + 1 < b and toks[a + 1].val == "(" \ and match_close(toks, a + 1, "(", ")") == b - 1 \ and toks[a].val in prog.funcs: name = toks[a].val if name not in owners: vals = set() for callee in prog.funcs[name]: # No call context here on purpose: the OWNED namespace is # every key the helper can ever produce, over all call sites. for rng in prog.returns_of(callee): p, _ = prog._expr(callee.toks, rng[0], rng[1], callee, 0, set()) vals |= {x for x in p if x} owners[name] = vals f2 = [] for s in prog.sites: if s.literal is None: continue for owner, vals in sorted(owners.items()): for v in sorted(vals): if covers(v, pat_exact(s.literal)): f2.append((s, owner, v)) break else: continue break unresolved = [s for s in prog.sites if s.unresolved or not s.pats] # Baseline signatures carry NO line number on purpose: an unrelated edit that # shifts a line must not un-mute an accepted finding (that is crying wolf), # but a GROWTH in count must not hide either. So a baseline entry is # ` [xN]` and only the first N matches are muted. baseline, bad_baseline = {}, [] for ln in read_decl(baseline_path): n, key = 1, ln parts = ln.rsplit(" x", 1) if len(parts) == 2 and parts[1].isdigit(): key, n = parts[0].strip(), int(parts[1]) baseline[key] = n def sig(path, code, detail): return "%s %s %s" % (path, code, detail) findings = [] for r, p in f1: findings.append((sig(r.path, "DEAD-READ", "%s:%s" % p), r.line, " %s:%d state_get(%s)\n resolves to %s %r — no state_set in the tree produces it" % (r.path, r.line, r.text, p[0].upper(), p[1]))) for s, owner, v in f2: findings.append((sig(s.path, "HAND-ROLLED", "%s<-%s()" % (s.literal, owner)), s.line, " %s:%d state_%s(\"%s\")\n %s() owns this key namespace (%s %r) — go through the helper, " "or a rename orphans this site silently" % (s.path, s.line, s.kind, s.literal, owner, v[0].upper(), v[1]))) findings.sort(key=lambda f: (f[0], f[1])) live, muted, budget = [], [], dict(baseline) for f in findings: if budget.get(f[0], 0) > 0: budget[f[0]] -= 1 muted.append(f) else: live.append(f) stale = sorted(k for k, v in budget.items() if v > 0) print("── state-key audit ─────────────────────────────────────────────") print("scanned %d .el files%s" % (len(prog.files), "" if include_tests else " (tests/ excluded)")) print("sites %d state_set, %d state_get" % (len(writes), len(reads))) print("keys %d distinct write patterns" % len(write_pats)) print("") if verbose: print("WRITE PATTERNS") for k, v in sorted(write_pats): print(" %-6s %s" % (k, v)) print("") if external: print("DECLARED HOST-SET (%d) — %s" % (len(external), external_path)) for k, v in sorted(external): print(" %-6s %s" % (k, v)) print("") print("UNRESOLVED (%d) — reported, never fails the build" % len(unresolved)) if not unresolved: print(" (none)") for s in sorted(unresolved, key=lambda x: (x.path, x.line)): print(" %s:%d state_%s(%s)%s" % (s.path, s.line, s.kind, s.text, " [partial: %s]" % ", ".join("%s %r" % p for p in sorted(s.pats)) if s.pats else "")) print("") if muted: print("BASELINED (%d) — pre-existing debt accepted in %s. NOT clean; fix these." % (len(muted), baseline_path)) for sg, line, _ in muted: print(" %s (line %d)" % (sg, line)) print("") if stale: print("STALE BASELINE (%d) — entries that no longer match anything; delete them:" % len(stale)) for sg in stale: print(" %s" % sg) print("") print("FINDINGS (%d)" % len(live)) if not live: print(" (none)") for _, _, body in live: print(body) print("") if live: print("FAIL: %d state-key finding(s). See scripts/verify-state-keys.sh " "for why this gate exists (issue #129)." % len(live)) return 1 print("PASS: every resolvable state_get key has a producer, and no key " "namespace is spelled two ways.") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))