# code — reasoning about a codebase. # # This module supplies the REASONING. You supply the facts: your own functions, # classes, calls and imports, written into your store as `code.Function`, # `code.Call`, and so on. Import this module and the schema, the standing rules # and the queries below apply to whatever you have loaded. # # import code # # Nothing here describes any particular codebase. Everything here describes how # to reason about one. class Function: name: string module: string lines: int branches: int loops: int deprecated: bool # What this fact is ABOUT — `python`, `cpp`, `rust`, whatever your # producer calls it. Optional, and absent-tolerant: a store that never # records it keeps working exactly as before. # # It matters once a store holds more than one language, which is the # normal case. `module` is a path, so it hints at language at best and no # query can rely on it; without this field a repository cannot be asked # about one half of itself. It is also where two producers become # distinguishable — and they may be counting `branches` on different # scales, since what counts as a branch is your producer's decision. language: string # A test that was seen to reach this function. `untested` below reads its # ABSENCE, so record it only where a test really does reach — an empty # value would silence the rule everywhere instead of where it should be # silent. Reaching is not executing, so this is not a coverage measurement: # read `untested` as "no test was seen to reach this", the same register as # `uncalled`. # # The opposite mistake is the one actually met, and it is worse because it # looks like an answer. Record this NOWHERE and the absence holds # everywhere, so `untested`, `critical()` and `unexplained()` each return # every function they range over instead of narrowing — three questions # collapsed onto one list, with nothing saying so. The usual cause is not a # decision about this field at all: it is a producer pointed at source # without the tests beside it. Both directions destroy the query. If an # answer here looks too round, `checkup` reports `unmet code.Function.test # no-values` when nothing is recorded in it, which is the difference # between "no test reaches these" and "no test was looked for". test: string # Results this function throws away — a call whose return value is not # bound, not tested, and not handed onward. Discarding a status is a # decision taken silently, and silence is what makes it worth reporting. returns_ignored: int # Something in this codebase calls this function. Recorded only where a # call was actually resolved to it, exactly like `test` above and for the # same reason: `uncalled` reads its ABSENCE, so a value written everywhere # would silence the rule instead of firing it. # # It is the same observation as a `Call` whose `callee` is this subject, # recorded on the callee's side as well. That looks redundant and is the # whole point: asking "does any Call name me?" is a question about another # kind, and a rule ranges over ONE kind (§15.1), so nothing can conclude # from it. Recorded here, the absence is a fact about this subject and # `uncalled` below is an ordinary rule. called: bool # Degree: how many DISTINCT functions call this one, and how many it calls. # Fan-in answers "how much breaks if I touch this"; fan-out answers "how # much this one depends on". # # Distinct neighbours, not call sites — a caller that calls you three times # is one dependency, not three, and counting sites would rank a function by # how chatty its callers happen to be. # # Recorded by your producer rather than counted here, for the reason # `called` above is: a rule ranges over ONE kind, so "how many Calls name # me?" cannot be asked of the Call kind, and counting it inside a query is # work proportional to your whole codebase for an answer proportional to # the answer. Unlike `called`, write it for EVERY function including zero — # it is read by comparison, not by absence, and a function nothing calls # has a fan-in of zero, which is a fact rather than a gap. # # Absent-tolerant like `language` and `test`: record neither and everything # else answers as before, with `hub` simply never concluding. callers: int callees: int # A reason for this function was written down — see `Rationale` below. # Read by its ABSENCE, exactly like `test` and `called`, so record it only # where a reason really exists: a value written everywhere would silence # `unexplained_critical` instead of firing it. explained: bool # Cyclomatic complexity: one path, plus one per branch and per loop. Kept # current by the engine as branches and loops are written. complexity: int = branches + loops + 1 class Class: name: string module: string methods: int # As on Function, and for the same reason: optional, absent-tolerant, and # what makes a polyglot store sliceable. language: string class Call: # caller and callee hold the SUBJECT of a Function, not its display name. # `uncalled` matches Call.callee against the subjects of Function, so a # display name here would make every function look uncalled — the queries # cannot tell a name from a subject, they only compare. Whatever you use as # the subject when you record a Function is what belongs here. caller: string callee: string site: string class UnresolvedCall: # A call whose TARGET COULD NOT BE NAMED — the inventory of what your # ingest could not see. # # No edge is asserted here, and none should be. A missing edge is a known # gap; a guessed edge is a wrong answer these queries cannot tell from a # fact. What this records is the other true thing: a call happened at this # site, and which function it reaches was not determined. # # It exists so `uncalled()`'s caveat becomes a number for YOUR store # instead of a warning about every store. A codebase whose call graph is # nearly complete and one that is mostly dynamic dispatch deserve very # different trust in the same answer, and until this kind existed there was # no way to tell them apart from inside. # # Absent-tolerant. Record none of these and everything else works exactly # as before; `dispatch_sites()` simply returns nothing. caller: string # the SUBJECT of the enclosing Function, as on Call site: string # file:line — where to look text: string # the callee as written, so you can see why it is here class Rationale: # A reason the author WROTE DOWN, attached to what it explains. # # Every other kind here is structure — something a reader could recompute # from your source. This is the part that cannot be recomputed: WHY the # structure is the way it is. Most codebases write it down and no tool ever # reads it. # # STATED, never inferred. `Counterpart` is the same move: two functions # that have to agree is a claim about intent, only a reader knows it, so # the reader states it and the engine holds the consequences. Inferring # intent would produce facts you cannot tell from the real ones. # # WHICH COMMENTS COUNT IS YOUR PRODUCER'S DECISION, not this module's, in # the same way that what counts as a `branch` is. Ours records `WHY:` and # `NOTE:` and nothing else. Whatever you choose, choose something the # author had to type on purpose: a convention wide enough to sweep ordinary # comments turns every line in your tree into a fact and this query into # noise. # # Absent-tolerant. Record none and every other answer is unchanged. subject: string # the SUBJECT of the Function or Class it explains marker: string # which marker it was, so you can tell them apart text: string # what the author actually wrote site: string # file:line — where to read it in full class Import: importer: string imported: string root: string class Dependency: name: string version: string direct: bool pinned: bool class Counterpart: # Two functions that have to agree — an encoder and its decoder, a writer # and its reader, two siblings that should guard their inputs alike. # # Agreement is not something a call graph can see. Nothing about # `word_encode` and `word_decode` says they are two halves of one contract; # that is a claim about intent, and only a reader knows it. So you state # the pair, and you state how much of the agreed set each side handles. # The engine then holds the difference, and keeps holding it: the day one # side gains a check the other lacks, the conclusion below appears without # anyone re-running an audit. # # `relation` names what is being counted, because it differs per pair — # validation predicates applied, radix values handled, verb cases covered. # `left_covers` and `right_covers` count members of THAT set, so they are # comparable to each other and to nothing else. name: string left: string right: string relation: string left_covers: int right_covers: int # Which way the asymmetry runs, and by how much. Zero is the healthy case # and concludes nothing — a pair that agrees is not a finding. # # Both directions are kept as their own field so each reads as a positive # magnitude where it applies. A claim's `.value` is not a number you can do # arithmetic on, so a query cannot negate `gap` itself; the engine derives # the mirror instead, and keeps it current for free. gap: int = left_covers - right_covers reverse_gap: int = right_covers - left_covers # ── Standing rules ─────────────────────────────────────────────────────────── # # A rule holds continuously: its conclusion is in force exactly while its # condition holds, and withdraws itself when the condition stops holding. You # never write these fields; the engine maintains them. @rule def hotspot(): for f in Function: if f.complexity > 10: f.hotspot = True @rule def oversized(): for f in Function: if f.lines > 200: f.oversized = True @rule def sprawling_class(): for c in Class: if c.methods > 20: c.sprawling = True @rule def hub(): # Many things depend on this one. The place a change is most expensive, and # the place a defect reaches furthest. # # `> 5` is a threshold, not a percentile, so it reads the same way as # `hotspot`'s `> 10` and the number is yours to argue with. It was measured # rather than picked: over two unrelated real codebases of ~190 functions # each, one Python and one C++, `> 5` selected 4% and 5% of functions — the # genuine hubs, without narrowing to a single name. A percentile would # always name someone, even in a codebase that has no hub at all. for f in Function: if f.callers > 5: f.hub = True @rule def unexplained_critical(): # Complicated, unreached by any test, AND carrying no recorded reason. # # `critical_untested` above already finds the first two. This is the # sharper object, and the difference is the question a reviewer asks first # about anything on that list: was this deliberate? A function whose # comment says why it is complicated and why it is not tested is a # different thing from one that says nothing, and until `Rationale` existed # no query could tell them apart. # # Two absences and one positive premise. The positive one is required — # a rule whose every premise is an absence has no set to range over. for f in Function: if f.complexity > 10 and absent(f.test) and absent(f.explained): f.unexplained_critical = True @rule def untested(): for f in Function: if f.lines > 0 and absent(f.test): f.untested = True @rule def uncalled_fn(): # Nothing was seen to call this. Sibling of `untested` in every way: an # absence, concluded once by the engine, so the query that reports it can # start from the conclusion instead of testing every function in turn. # # `f.lines >= 0` is the range, not a filter — a rule whose every premise is # an absence has no set to work over and is refused. Every function carries # `lines`, so this admits all of them. for f in Function: if f.lines >= 0 and absent(f.called): f.uncalled = True @rule def critical_untested(): # Complicated AND unreached. `hotspot` and `untested` each report half of # this, and reading two lists side by side is how the intersection gets # missed. An empty answer here is a good result worth having. for f in Function: if f.complexity > 10 and absent(f.test): f.critical_untested = True @rule def discards_result(): for f in Function: if f.returns_ignored > 0: f.discards_result = True @rule def left_stricter(): # The left side of the pair checks more than the right. Read it as: the # right side accepts input the left side would have refused. for c in Counterpart: if c.gap > 0: c.left_stricter = True @rule def right_stricter(): for c in Counterpart: if c.gap < 0: c.right_stricter = True # ── Queries ────────────────────────────────────────────────────────────────── # # A read is bound to a name before it is used, one step per line. That is the # language's house style and it keeps a query readable as a sequence of facts. def callers_of(fn: string) -> list: # Every function that calls `fn`, directly. out = [] cs = subjects(Call.callee, fn) for c in cs: who = current(Call.c.caller) name = str(who.value) out = out + [name] return out def callees_of(fn: string) -> list: # Every function `fn` calls, directly. out = [] cs = subjects(Call.caller, fn) for c in cs: whom = current(Call.c.callee) name = str(whom.value) out = out + [name] return out def impact_of(fn: string) -> list: # Who breaks if `fn` changes: its callers, and their callers, to depth 3. # # Reachability is walked to a fixed depth rather than to a fixpoint. Every # loop here runs over a finite set drawn from the store, which is what makes # a query guaranteed to finish. Three hops covers the reviewing case; a # deeper reach means asking again from a name this answer surfaced. out = [] first = subjects(Call.callee, fn) for c1 in first: a = current(Call.c1.caller) av = str(a.value) if av not in out: out = out + [av] second = subjects(Call.callee, av) for c2 in second: b = current(Call.c2.caller) bv = str(b.value) if bv not in out: out = out + [bv] third = subjects(Call.callee, bv) for c3 in third: d = current(Call.c3.caller) dv = str(d.value) if dv not in out: out = out + [dv] return out def importers_of(mod: string) -> list: # Every module that imports `mod`, directly. out = [] ims = subjects(Import.imported, mod) for i in ims: who = current(Import.i.importer) name = str(who.value) out = out + [name] return out def functions_in(lang: string) -> list: # Every function recorded as belonging to one language. # # A store holding more than one language is the normal case, and without # this there is no way to ask about one half of it. Functions whose # producer recorded no `language` are absent here — the field is # optional, so this reports what was stated rather than assuming a # default that would put every unlabelled function in whichever language # was asked for. out = [] fns = subjects(Function.language, lang) for f in fns: out = out + [f] return out def hotspots_in(lang: string) -> list: # Complicated functions in one language — "the hotspots in the Rust half". # # The walk starts from the CONCLUSION, not from the language: `subjects` # matches what a rule concluded as readily as something you wrote, so # this costs work proportional to the hotspots rather than to the # codebase. Starting from the language instead would walk every function # in it, which on a single-language store is all of them. # # `language` is optional, so the read is guarded. A function whose # producer recorded none is absent from every language's answer rather # than present in all of them. out = [] hot = subjects(Function.hotspot, True) for f in hot: spoken = current(Function.f.language) match spoken: case active_claim: if str(spoken.value) == lang: out = out + [f] case empty: pass return out def uncalled() -> list: # Functions nothing calls — dead code, or an entry point. # # A name with no incoming call. Whether that means dead or means `main` is a # judgement about your codebase, not about the call graph, so this reports # and does not conclude. # # It also depends on how complete your call facts are. A call your ingest # could not see — one made through a variable, a handler table, or any # dispatch decided at run time — leaves its callee looking uncalled here. # Read this as a list of candidates to look at, never as a list to delete. # # Starts from the CONCLUSION, like `hotspots` and `critical`. It used to # walk every function and ask the store, per function, whether any call # named it — work proportional to the codebase to produce an answer # proportional to the dead code in it, and on a 144-function store that was # the query that overran the wire. The `uncalled` rule concludes it once # instead, from the `called` fact the producer records on the callee. out = [] fns = subjects(Function.uncalled, True) for f in fns: out = out + [f] return out def hubs() -> list: # What breaks most if you touch it — the functions most depended on, most # first. # # Starts from the CONCLUSION, like `uncalled`, `critical` and `hotspots`: # the `hub` rule has already decided which functions qualify, so this walks # the answer rather than the codebase. Ranking every function instead would # be the shape that once overran the wire. # # Ranked here rather than by you, because a list you have to sort yourself # arrives in an order that means nothing. Sorting a `(degree, subject)` # tuple descending orders by degree and settles ties by subject, so the same # store always answers in the same order. # # Reports the SUBJECT, not the `name` field — the identity `uncalled()` # returns, so the two lists join. Reading `name` would put a second # absent-read on the identity itself: a producer recording degree but no # display name would have its hubs silently vanish from here while # `uncalled` still named them, and a missing hub is indistinguishable from # a codebase that has none. ranked = [] fns = subjects(Function.hub, True) for f in fns: c = current(Function.f.callers) match c: case active_claim: # `int(str(…))` is not a redundant round trip. A claim's # `.value` is a `value`, deliberately not a number you can do # arithmetic on — the constraint `Counterpart.gap` records, and # why the engine derives that mirror rather than a query # negating it. `int()` refuses a `value` directly and `str()` is # the one conversion it takes. Sorting the string instead would # order "10" before "9". ranked = ranked + [(int(str(c.value)), f)] case empty: pass out = [] for pair in sorted(ranked, reverse=True): out = out + [str(pair[1]) + " " + str(pair[0])] return out def rationale_for(fn: string) -> list: # Why this function is the way it is, in the author's own words. # # Takes the SUBJECT you recorded the function under, the same identity # `callers_of` and `impact_of` take. out = [] rs = subjects(Rationale.subject, fn) for r in rs: mk = current(Rationale.r.marker) tx = current(Rationale.r.text) match tx: case active_claim: match mk: case active_claim: out = out + [str(mk.value) + ": " + str(tx.value)] case empty: out = out + [str(tx.value)] case empty: pass return out def unexplained() -> list: # Complicated, untested, and nobody wrote down why. # # Read this instead of `critical()` once you record rationale: it is the # same list with everything already accounted for removed. An empty answer # here over a long `critical()` is a good result — it means the risk is # known and written down rather than absent. # # Until you record any, this CANNOT narrow below `critical()` and returns # exactly that list, which is the one case where the advice above is not # yet followable. That is a fact about your codebase and not a missing # producer: `Rationale` is written from `WHY:` and `NOTE:` comment markers, # so a tree carrying none records nothing here and both queries answer # identically. `checkup` names it as `unmet code.Function.explained # no-values`, and a producer that wrote none says so on stderr as it runs. out = [] fns = subjects(Function.unexplained_critical, True) for f in fns: out = out + [f] return out def dispatch_sites() -> list: # Where static reading ended — every call whose target was not determined. # # Read this beside `uncalled()`. That query lists functions nothing calls, # and its honest caveat is that a call your ingest could not see leaves its # callee looking uncalled. This is that caveat, counted: each entry is a # place where a call happens and the target is unknown, so any of them # could name any function in the list `uncalled()` gave you. # # Nothing here is a defect on its own. Dynamic dispatch is how a plugin # table, a callback, a virtual override and a decorator all work. The # number is a measure of how much of your call graph is decidable by # reading, not a list of things to fix — but it is also the place a # reviewer most wants to look, because it is exactly where reading stops # telling you what runs. # Both reads are guarded, because a producer is not obliged to fill every # field and a query that raises on a half-written subject is worse than one # that reports what is there. `site` is the entry: without a location there # is nothing to go and look at, so that subject is skipped. `text` is the # detail, and its absence costs you the source snippet, not the row. # Guarding is free — the meter charges what you WRITE, and these are reads. out = [] us = list_subjects(UnresolvedCall) for u in us: where = current(UnresolvedCall.u.site) match where: case active_claim: what = current(UnresolvedCall.u.text) match what: case active_claim: out = out + [str(where.value) + " " + str(what.value)] case empty: out = out + [str(where.value)] case empty: pass return out def unused_dependencies() -> list: # Dependencies you declared that nothing imports. # # Matched by NAME, which is where this can be wrong in one direction: a # distribution whose import name differs from its package name — Pillow is # imported as PIL — looks unused here. So this is a list to check, not a # list to uninstall, for the same reason `uncalled` is. # # Only dependencies you DECLARED are considered. A transitive one — pulled # in by something else, and carrying no `direct` — is not expected to be # imported by your code, so counting it here would report your whole # dependency tree as unused. # # A dependency with no `name` is SKIPPED rather than faulting the query. # `name` is what `Import.root` is matched against, so there is nothing to # ask about one without it — but one half-written subject must not take out # the answer for every complete one beside it. Same call `dispatch_sites()` # makes about a site with no location. out = [] deps = subjects(Dependency.direct, True) for d in deps: n = current(Dependency.d.name) match n: case active_claim: nm = str(n.value) users = subjects(Import.root, nm) c = users.length if c == 0: out = out + [nm] case empty: pass return out def unpinned_dependencies() -> list: # Dependencies declared as a range rather than an exact version. Whether # that is a problem is your call; what the store can say is which they are. # # Nameless subjects skipped, as in `unused_dependencies` above. out = [] deps = subjects(Dependency.pinned, False) for d in deps: n = current(Dependency.d.name) match n: case active_claim: out = out + [str(n.value)] case empty: pass return out def asymmetries() -> list: # Every pair whose two sides do not handle the same amount, and which way # the difference runs. A pair that agrees is absent, because agreement is # not a finding. # # `subjects` matches a CONCLUSION as readily as something you wrote, so this # asks for the pairs that concluded and walks only those. Walking every pair # to test each one costs work proportional to your store rather than to the # answer, which on a codebase of any size is the difference between an # answer you can read and one too large to come back. # # A pair missing `name` or `relation` is SKIPPED rather than faulting the # query, the way `dispatch_sites()` skips a site with no location — one # half-written pair must not take out the answer for every complete one. # # The two magnitudes need NO guard, and that is checked rather than # assumed: both rules below conclude from `gap`, which is computed from # `left_covers - right_covers`. So a conclusion in force means `gap` # derived, which means both operands are present, which means # `reverse_gap` derived too. Walking the conclusion is what guarantees # them — reading either magnitude without walking one would not. out = [] left = subjects(Counterpart.left_stricter, True) for c in left: n = current(Counterpart.c.name) r = current(Counterpart.c.relation) g = current(Counterpart.c.gap) match n: case active_claim: match r: case active_claim: out = out + [str(n.value) + " [" + str(r.value) + "] left stricter by " + str(g.value)] case empty: pass case empty: pass right = subjects(Counterpart.right_stricter, True) for c in right: n = current(Counterpart.c.name) r = current(Counterpart.c.relation) g = current(Counterpart.c.reverse_gap) match n: case active_claim: match r: case active_claim: out = out + [str(n.value) + " [" + str(r.value) + "] right stricter by " + str(g.value)] case empty: pass case empty: pass return out def discarded_results() -> list: # Functions that throw a result away. Each one is a place where a failure # cannot reach the caller. out = [] fns = subjects(Function.discards_result, True) for f in fns: out = out + [f] return out def critical() -> list: # Complicated and unreached by any test — where a defect is both likeliest # to be and likeliest to survive. # # Read `unexplained()` beside this once you record rationale: it is this # list with everything already accounted for taken out. out = [] fns = subjects(Function.critical_untested, True) for f in fns: out = out + [f] return out