Skip to content

v0.6.9

Latest

Choose a tag to compare

@github-actions github-actions released this 29 Oct 16:28
· 453 commits to main since this release
bedc0cd

ripwire 0.6.9: better answers to the questions agents ask (a --path that prints the bodies it names, call chains and callers that earlier builds left out), and more hedged or declined calls instead of confident wrong ones. This release collects twelve merged batches of reviewed changes (pull requests #424 to #463). The next section shows real before and after output; section 1 is about honesty (hedged or declined calls, floors named, the bodies --path prints); section 2 is the new --defs verb and Owner.name selectors; section 3 is language specifics and callers for error-shaped --for questions. Every item is an entry in the changelog, which states what it was measured on and what it does not show; statements here are in-sample, and this page makes no comparison with any other tool.

Better answers to the questions agents ask

Each line is a question from the sets we developed ripwire against (in-sample, not held out), run with the same command on 0.6.8 and 0.6.9; sizes are the whole answer.
Eight of these nine are questions whose answer spans several files or runs through indirection: an interface or trait call, a message queue, a renamed import.

Across files: call chains, callers and the code on the route

  • TypeScript (hono): how does fetch reach compose? 0.6.8 named the hops and printed no code (1,005 B). 0.6.9 also prints the middle hop's body, where the compose( … ) call sits, and names each method's class (Hono.#dispatch; 3,973 B).
  • C (htop): ScreenManager_run to LinuxMachine_scanCPUTime. The hedged route was named without its code; now the three bodies come with it (3,444 to 10,699 B).
  • Go (terraform): the call chain from Context.Plan to Graph.walk. 0.6.8 printed no chain (22,049 B). 0.6.9 appends the six-hop chain, Context.PlanAndEval, plan, planWalk, walk, Graph.Walk, Graph.walk (24,389 B).
  • Rust (nushell): the call chain from main to eval_block_with_early_return when a script file runs. 0.6.8 printed no chain (23,599 B). 0.6.9 appends the five-hop chain through run_file (28,310 B).
  • Go (cockroach): which non-test functions call NewReplicaEvalContext? 0.6.8 named none of the four callers (21,812 B). 0.6.9 appends a callers answer that names all four, with their call lines (24,928 B).

Through indirection: dispatch, message queues, renamed imports

  • Python (textual): how does post_message reach the on_* handlers? 0.6.8 said "not reached", with no route (3,480 B). 0.6.9 prints a route marked as a hedge (proven="0") through the message queue to _dispatch_message, with the four bodies (13,973 B).
  • Rust (mdbook): what does execute_build_process call? A call on a &dyn Renderer was bound by name to the test stub TestRenderer in the same file (render, name; 1,526 B). Now it is declined and says what could run: <dc n="render" l="207" cands="5" via="declined" gate="iface"/>, five renderers (2,000 B).
  • Python (cpython): who calls re.sub? Calls written from re import sub as re_sub were not counted; now TimeRE.pattern and LocaleTime.__calc_date_time are (73 to 75 callers).

Unused code

  • Python (textual): which functions in _segment_tools.py have no caller? 0.6.8 printed count="0" (1,025 B). 0.6.9 adds <to n="align_lines">, a function only tests call (1,374 B).

On the 163 questions we developed against, same command on both versions, graded against our answer key: complete answers went from 112 to 129, partial from 38 to 31, missing from 13 to 3; total bytes per complete answer went from 16,523 to 15,099 (all answers together are 5% larger). One answer moved from complete to partial (ninja-14): 0.6.8 reached it by guessing one of three implementations by name; 0.6.9 shows the route as a hedge instead. Proving which implementation runs is planned work. These are in-sample numbers, not a prediction for other code.

Read this first

  • Caches are rebuilt once. Every ingest cache and quality snapshot an earlier build wrote is refused and re-indexed on first run (parser version 259, cache version 46, quality-snapshot scheme 24).
  • Scripts that read n=: a member row on --callers, --callees, --impact, --path and signature rows now prints its owner (Pattern.match, Builder::Build in C, C++ and Rust, Task#execute in Ruby); the spelling is one every symbol verb accepts, so a row can be pasted back. On a signature <d> row the owner folds into n= and sc= is dropped. --json keys, <c> callee rows, tail rows and the map are unchanged.
  • A file:LINE selector that two definitions start on now refuses and lists each candidate on stderr, instead of binding the innermost one without saying so.
  • --dead-code prints a new <to> row group after its <d> rows (library functions whose every indexed caller is test code); they are not in count=. --affected adds a separate hedged group (via="class-new") after the proven rows.
  • --situ and --test-gate start the caller walk from the changed symbols, not from every symbol of every changed file. A hunk that no function body fully holds sends its whole file back to the old behaviour.
  • --for and --path answers gain elements and attributes (<callers>, <graph_verb>, <ep>, <alts>, <bodies>, slots=, mem=, ctor_l=, stem="1" on the map); each is documented in its changelog entry and in the answer's own legend. Environment switches that turn a new behaviour off are named in the entries (for example RIPWIRE_NO_HOP_BODY=1, RIPWIRE_NO_CTOR_HOP=1, RIPWIRE_NO_MAP_STEM=1).
  • Contributors: a changelog entry is now one new file under changelog.d/, folded into CHANGELOG.md at release.

1. Honesty first: hedged or declined instead of confidently wrong

Each item says what changes for an agent reading the answer. These are in-sample statements: each changelog entry names the repositories and rows it was checked on and what it does not show.

  • A name match is labelled as one. A by-name hedge to a free function or a JavaScript/TypeScript method now needs the caller's file to be, or import, the namesake's file; an override counts as a way in. Hedged hops (via="name", via="name-key", via="queue", via="slot") are never counted in reaches=, reachable= or hops=, so those counts hold proven rows only.
  • Fewer wrong edges, stated as unresolved. A JavaScript/TypeScript call through a name written after its declaration (Eng = Foo; new Eng()) is unresolved=, not the declared class. A Rust &dyn Trait call never binds a same-file impl by name when the trait's implementor list is open. Widget.run no longer binds a class's method when an object literal named Widget has a run too; the spelling is refused instead. this.f.m() on a typed field never binds the caller's own m. A Kotlin trailing-lambda let, run, with, apply or also stays off a Java method of the same name (#438, @llvm-x86).
  • A floor is named instead of a confident zero. A call through an interface-, trait- or abstract-typed receiver names the implementors that can run, and the interface carries implementors_floor= when the list cannot be complete. --lego stops listing an implementor whose own file imports its base from outside the tree. --grep says when a literal pattern reads as a regex (pattern_meta=) instead of answering hits="0"; --whereis stops labelling comment, prose and string lines kind="def"; a file:LINE selector that two definitions start on refuses and lists each candidate instead of binding the innermost one silently.
  • --dead-code and --safe-delete stop over-reporting. static counts as internal linkage only in C, C++ and Objective-C, and Django and unittest hooks are not reported as dead code (#437, @llvm-x86; reported in #228 by @hnipps). New <to> rows list library functions only tests call, apart from count=.
  • --path prints the bodies that hold its answer. A short --path prints the body of its middle hop (whole up to 4,000 bytes, then cut and said so). An unreached --path that names a route prints the bodies of that route's bridge rows and readers, marked proven="0". It lists every reaching definition pair when a name has several, the other definitions of an endpoint whose pick is not provably unique (<alts>), prints first the pair whose owner you named, and says when it bound a test or fixture copy of a name (from_test=, to_test=).
  • Hedged hops compose. --impact counts who reaches a hedged caller, folds a hub in the hedged ring into one row, and --affected adds tests that construct a reached class as a separate hedged group (via="class-new").

2. --defs and Owner.name selectors

  • New verb: --defs=NAME[,NAME…]. One call answers which definitions up to 16 names bind: each row has the owner-qualified name, kind, file:line, language, source or test, its callers (a floor), and sel=, a spelling that picks that definition alone, so the next call needs no guessing.
  • Rows name their owner. A member row on --callers, --callees, --impact, --path and signature rows prints Pattern.match, Builder::Build (C, C++, Rust) or Task#execute (Ruby), and every such spelling can be pasted back as a selector.
  • Names resolve as the source spells them. Class.method in TypeScript, json.dump, batcheval.Put, ns::Outer::Inner::f and Hono.#dispatch now resolve; a JavaScript/TypeScript method row names its class; a bare module.exports = (…) => … is a function named by its file. A bare name with several definitions is ordered the same way in every verb, and --lsp says when a name has several (#432, @mpapis).

3. Language specifics, and callers for error-shaped questions

  • Rust. A receiver written as a type (&Book, Box<T>, a path, a struct field) proves that type's method. Functions stored in struct fields are joined to the (x.f)( … ) calls through them, in --callers, --callees and --for. Files using an empty + use<> bound no longer lose every impl owner.
  • Python. A call through a renamed import (from m import f as g) reaches m.f, also when the module defines __getattr__. Annotated fields (self.f: T | None) and mod.X( … ) constructors type a receiver. A posted message class names its handlers (<handler_rule> on --path, via="name-key"), and a queue put hands off to the get readers (via="queue"). --for shows how a shown definition is registered through a manifest entry point.
  • Go, C and C++. A function stored in a struct field (a Go keyed element, field assignment or registrar argument; a C .exec = cmd_split_window_exec) is joined to the dispatcher that calls it.
  • JavaScript and TypeScript. A method row names its class; --uses on a literal receiver no longer lists built-in member calls (#411, @lennix1337).
  • Error-shaped --for. A --for task about an error (error, exception, panic, raise, throw, fail, crash, Traceback, or an error type written as one identifier) with no function named carries the same <callers> chain the name-exact route carries, marked by="rank", for the narrow function it ranks in rows 1 to 3.
  • More of what a --for question points at. It follows a one-line wrapper into the function it calls, names what the top method's class constructor calls (ctor_calls=), the functions a shown table initializer binds (slots=) and the members of a shown owner (mem=), lists the callers of a function it is about, and answers a task spelled as a graph question (path from A to B, callers of X) with that verb's answer, honouring --token-budget.
  • Contexts that changed. --situ and --test-gate start from the changed symbols, not every symbol of every changed file (#423, @llvm-x86).

Platforms and safety

  • Windows: --quality-delta=A..B extracts its per-commit trees through stdin, so a drive-letter path no longer fails (#402, #422, @lennix1337); clang-cl PGO builds link and keep SLP vectorization in the final binary (#410, @lennix1337).
  • The pattern screen that guards --regex, --graph-query file(), --arch path rules and #match? refuses an overlapping alternation under an unbounded quantifier, one group deeper than before (#397, @pt-act).
  • Gates that timed out on slow runners were made cheaper, with no change to the product (macOS impact-partition and C++ localization gates, memguard and pargates arms, the Linux ASan alarm, #421). A test arm of the type-3 clone gate was renamed so the gate checks it (#431, @liziye627-design; part of the cleanup @s0undt3ch asked for in #305).

Known floors

  • Every via="name", via="name-key", via="queue" and via="slot" row is a hedge, not a proof: it is never counted in reaches=, reachable= or hops=, and the readers or candidates it lists are a floor.
  • A single pathological regular-expression match remains unbounded on libstdc++, tracked as regex-screen-multiquantifier.
  • Not joined: a Rust closure stored in a field, a method-call registrar (r.set( f )) and a function value inside a macro; Go interface values (Go interfaces are structural); C typedef'd function-pointer fields copied between structs.
  • Kotlin: a parenthesised lambda (s.let( { } )) is not a trailing lambda, and a Java method called from Kotlin with a trailing lambda is dropped when its name is a scope-function name.
  • Python: a package that only re-exports a function, or an import inside another function, keeps the earlier answer for a renamed import.
  • In MCP for, a middle-hop body the answer already prints can print a second time, because that twin has no served-body set to compare (a known divergence, named in the changelog).

Thanks

Code: @lennix1337 (#402, #410, #411, #422: Windows --quality-delta, clang-cl PGO builds, --uses on literal receivers), @llvm-x86 (#423 --situ and --test-gate from changed symbols; #437 Django and unittest hooks; #438 Kotlin scope functions), @mpapis (#432: one order for every multi-definition name answer, and --lsp says when a name has several definitions), @pt-act (#397: pattern-screen hardening) and @liziye627-design (#431: a clone-gate test arm fix). Reports that led to fixes: @hnipps (#228, Django and unittest hooks reported as dead code) and @s0undt3ch (#305, duplicated test helpers).

Versions and caches

Parser version 259, cache version 46, quality-snapshot scheme 24; the legend dictionary is dictv=05b48ed93c807ee2 entries=981. Caches written by 0.6.8 or any earlier build are rebuilt once.

Full notes: CHANGELOG.md, section 0.6.9.