Repository navigation
Python test selection: library callback summaries, seven recall fixes, no dispatch cap - #1885
Merged
swapnilpaliwal-sd merged 48 commits intoOct 10, 2026
Merged
Conversation
…class component tag hands over render and its lifecycle
A function written inside a JSX `{…}` — `onPointerMove={this.handleMove}`, `onClick={onSave}`,
`renderItem={drawRow}` — was reached by nothing. The parser writes each `{…}` as an expression root
of its own, linked to no element (an intrinsic tag has no call site at all), so no hand-over rule saw
it, and a handler read off a field or named by reference was cut off from every test that renders the
component and fires the event. The `{…}` is now the site that hands the function over, from the
function it is written in (callback_registered, as for `xs.map(cb)`); what the value is read through is
followed exactly as for an argument. A call there hands over what it returns and stays out.
A tag that builds a class component resolved only to its constructor, so render and the lifecycle
methods the renderer calls (componentDidMount, componentWillUnmount, …) had no caller, and nor did
anything below them. The tag now hands them over too, for a class that has a render method.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
A package's public functions are often its default instance's methods exported as consts: `export const createDraft = immer.createDraft.bind(immer)`, `export const plain = engine.plain`. The holder rules already knew what such a const holds (a method read as a value, through `bind`), and a call to it in the defining module ran that method. A call from any other module names the const through an import, and only a local variable, a parameter or a field was followed — so every caller outside the defining module, its whole test suite included, ran nothing. A call whose callee is an imported const, or a namespace import's member (`api.applyPatches(…)`), now runs what that module's own variable holds. The import is followed to its module, never matched by name: the same exported name bound in another module runs that module's method. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… of a project method
With no standard library staged, `JSON.parse(s)`, `Promise.resolve(v).then(f)`,
`Object.keys(o).forEach(g)`, `Reflect.getMetadata(k, t)`, `document.createElement('a')` and
`console.error(e)` resolve to nothing, so every one was an untyped `x.parse()` to impact: a project
method that happens to be named parse, resolve, all, keys, error or getMetadata got every such line in
the repository as a by-name caller, and every test around those lines entered its closure.
The engine now traces the receiver to the platform's global object (an AMBIENT_GLOBAL reference to
JSON, Promise, Object, Reflect, Array, document, console, …), and what calling it returns, the way it
already traces a package's value: the site stays unresolved, is listed apart as a call on a package's
or the platform's value, and seeds no closure. A program that declares one of those names itself binds
it as its own variable and is unaffected; window, globalThis and self are left out, since a program may
hang its own functions on them. An untyped receiver stays a by-name caller.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…tion beside it
The compiler's resolver prefers a declaration file: require('../') in a package whose
package.json names `types` (or an exports "types" condition), and import './x.js' beside
a hand-written x.d.ts, both resolved to the .d.ts. JavaScript never loads a declaration,
so every value imported that way was external and every call through it an untyped
receiver: a test of the package's own API reached nothing in it.
When the first answer is a declaration outside node_modules, the import is resolved again
with declarations hidden from the resolver (types/typings and the .d.ts substitution fall
through to main, the exports code conditions and the .js itself). A module that exists only
as a declaration keeps its first answer.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…raint and an object-type alias
`plugins.map((p) => p.setup(app))` left `p` with nothing to dispatch on in two common shapes:
- the element is an object type written as an alias (`type Plugin = { setup(): … }`): the no-lib
callback binding gave the parameter the element's TYPE only, and an alias of an object type has no
type declaration, only members — so every call on it was unresolved, while an annotated parameter of
the same type resolved through its shape. The callback parameter now takes the element's shape too.
- the list is typed by a type variable constrained to an array or tuple
(`<Ms extends [Module, ...Module[]]>(...modules: Ms)`): its element had no type at all. The
constraint is what every element is at least, as it already is for a `<T extends HasId>` receiver;
the element is now read through it, for callbacks and for `for…of`, by the exact link to the type
parameter or by its name in scope.
A plugin/module system built this way — a builder that maps over its modules and calls `init` — was
reached by none of the tests that drive it.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…ments flow The parameter fan cap refuses argument flow into a function called from more than --dispatch-cap places, and it counted every site, tests included. A suite calls the API it tests over and over, so exactly the functions under test went "hot": a constructor called from 21 test files beside 13 library sites lost its parameters, a callback the library handed it and stored on the instance (this.decode = options['decode']) was never followed, and the library's own call through the field resolved only to the fallback beside it. Two verdicts now. Whether the PROGRAM's arguments flow is decided by the program's own sites; whether a TEST's arguments flow is decided by every site, as before, so nothing that flowed before stops flowing: a plugin a test hands the program, which the program calls back with its own class, still types what the test then calls (a version that dropped every test argument lost exactly that; the case guards it). For the name count, the overcount used for module-level functions, a test's bare call or `new` of the name leaves it, while a test's member call that only shares the name (`lib.add(1, 2)` on an export that is a different function, such as a curried closure) stays in it: dropping those too made a curried library's graph three times larger and its index five times slower, for no change in any answer. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…e was handed
`export default promisable(eachItem, 3)`, `const add = curry2(function add(a, b) {...})`:
the wrapper returns a closure that calls its parameter, and the module exports that
closure. The closure's call of the parameter is read context-insensitively, so it held
every function any of the wrapper's sites passed (past the fan cap: nothing), and a test
calling the export reached the closure and stopped there.
The wrapping site is already a value of its own (("wrap", site), value-flow.dl), used for
what a call through it returns. A call through it now also runs the function that site was
handed at the forwarded position, what the wrapper writes into that parameter, or what a
wrapped argument itself runs: one of a set, beside the edge to the closure the call keeps,
and under the dispatch cap (a parameter every wrapped export is passed to runs one of them,
and naming all of them says nothing). A wrapper whose closure never calls its parameter is
not a wrapper and runs nothing it was handed. Two engine goldens gain these edges (memoize
/ once / lazy results and the param-reassigned shapes), reviewed and blessed with --oracle.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…it resolves to the .py The project linker keys modules by qualified name, and a package that ships `impl.pyi` beside `impl.py` declares that name twice. The later file won the map, and `.pyi` sorts after `.py`, so `from .impl import *` and `import pkg` resolved to the stub. The engine refuses a stub declaration as an edge target, so every call into such a package ended at the library boundary and its tests reached nothing in it. A stub no longer replaces a non-stub module of the same name. A stub with no `.py` beside it is still the import's target. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
pytest fills a test's parameter with whatever the fixture of that name returned or yielded, and no call site spells that call, so the parameter stayed untyped: every method called on it was ambiguous_unknown, and everything derived from it (a local assigned from one of those calls, a with block) went with it. Across ten public suites 1,529 such calls were unresolved; a suite whose fixtures are annotated already resolved its own. value-flow.dl now treats the runner's call as one more argument reaching a parameter: param_arg_type from the fixture's value type (its return, its yield, or its declared return). The fixture serving a parameter comes from a syntax-only copy of the runner's lookup (class, own module, nearest conftest): the existing py_fixture_injection reads module_member_method for star-imported fixtures, which is resolution, and its nearest-conftest MAX would then be a cyclic aggregate. Star-imported and pytest_plugins fixtures still reach their tests through the injection edge; they only stay untyped. Measured with a behavioural oracle (break each of 600 sampled functions, record which test files fail) on ten held-in repositories: impact --tests recall 0.668 -> 0.698, every-failing-file-selected 46.7% -> 50.5%, precision unchanged; the three repositories carrying the pattern move, the seven without it are byte-identical. Engine suite 43/43 with identical case results; query cases 310/310. New case fixture-value-types-the-parameter fails on the base engine on its derived-local check. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…(T, wrapper) returns wrapper functools.update_wrapper(wrapper, wrapped) hands back `wrapper`, and typing.cast(T, x) hands back `x`; both are documented, neither module is staged in a client-only run. A decorator written `return update_wrapper(wrapper, f)`, or typed as `return t.cast(F, update_wrapper(wrapper, f))`, was therefore opaque: every method it decorates was decorator_replaced_target and no call through the attribute reached anything. That shape occurs in five of sixteen public repositories measured. A catalogue, py_returns_arg(DottedName, Position) in builtins.dl, lists the two functions and the argument each returns; call_chain.dl follows such calls (to any depth) to the returned name, matched through the import that binds the callee: a module import, its alias (`import typing as t` is MODULE_IMPORT_ALIAS, which the copy.copy rule beside it also misses), or a from-import. On ten held-in repositories, path's found rate on runtime-proven chains goes 0.720 -> 0.744 (the repository with the shape: 0.52 -> 0.76); test selection and the other verbs unchanged. Engine suite 43/43 with identical case results; query cases 313/313; case decorator-returning-update-wrapper fails on the base engine on both decorator checks. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…y the import walk impact's `at import` rung names every test file importing a module whose BODY reaches the change through calls that run, because a module that raises while being imported fails every file importing it. A module body's calls inside `if __name__ == "__main__":` are real edges, but they run only when the file is executed as a script, never on import. Walking them named every importer of a module that ends in a demo block. The fact export now records guard_only(module, callee) where every call site the module body has to that callee lies inside a top-level main guard, and up_running does not take those edges. Ordinary reachability is unchanged; only the import hop is. Measured with the mutation oracle on ten held-in repositories, the at-import rung was the second-noisiest (precision 0.16 over 720 selected test files), and 637 of those 720 were for targets that never run on import. On the repository carrying the shape its at-import selections drop 713 -> 305 and test-selection precision 0.335 -> 0.373 with recall unchanged; the control repository whose at-import selections were all correct is unchanged. Query cases 315/315 python, 264/264 typescript, 333/333 java; case main-guard-does-not-run-on-import fails on the base engine on the guarded check. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…_"`) is a guard too `if sys.platform != "win32" and __name__ == "__main__":` is false on import whatever the other conjunct is, so the block cannot run then; an `or` could, and is not read as a guard. The first guard rule missed the conjunction, which on the repository carrying the shape was where almost all of the remaining false at-import routes came from. There: at-import selections 305 -> 55, of which 53 fail under the mutation oracle; test- selection precision 0.373 -> 0.446. Recall 0.810 -> 0.794: a few failing files had been selected only through an at-import route that cannot run, right by accident; their real route is one the graph does not see. Control repository unchanged. IMPACT_VERSION 65, so fact caches written under 64 are not reused. Case extended with the conjunctive spelling; query cases green. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…hat builds the object, not from every constructor impact takes a hop from a protocol member (a dunder no call site names) to whoever constructs its type: you do not build a context manager without entering it. That holds for __enter__/__exit__, __bool__, __hash__, __call__ (precision 0.61-0.80 under the mutation oracle), and not for showing a value (__repr__, __str__, __format__), pickling or copying it (__getstate__, __setstate__, __reduce__, ...) or deleting from it (__delitem__, __delattr__, __del__): those run only when some code asks, and a constructor deep inside other code says nothing about that. Measured on ten held-in repositories, __repr__ alone was 124 selected test files at precision 0.05, across eight of them. For those members the hop is now taken only from test code that constructs the type: a test that builds the object is the one that asks for its repr. Dropping the hop outright removed 403 false files but left 13 targets with a failing test and an empty answer; this form keeps every answer non-empty. Ten held-in repositories (this commit with the two main-guard ones): test-selection precision 0.408 -> 0.454, recall 0.698 -> 0.694, empty answers unchanged at 84/535. Case on-demand-dunder-hop-from-tests fails on the base engine on the __repr__ check; its control (__eq__ keeps every constructor) passes on both. Query cases 318/318 python, 264/264 typescript, 333/333 java. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…setter and deleter The engine emitted a PROPERTY_READ edge for reading a @Property and nothing for writing or deleting one, though `obj.x = v` runs `@x.setter` and `del obj.x` runs `@x.deleter` exactly as a read runs the getter. A setter therefore had no caller: an impact on it named no test, and everything the setter calls was cut off from whoever assigns. Setters occur in five of sixteen public repositories measured; the torture fixture documented the gap as a known miss. type_property_accessor finds the setter / deleter on the class that wins the name in the receiver's MRO (they share the getter's binding, so mro_lookup's single answer is not enough); property_write_edge keys on the attribute access's STORE / DEL context; the edge is exported as call kind PROPERTY_WRITE (schema vocabulary for Python, the kind TypeScript already uses for its accessors), mapped to the `property` rung. Torture: the CPython oracle confirms the new edge (agree 587 -> 588, missing 47 -> 46, extra unchanged); f43's known-miss marker is removed with its docstring rewritten at the same line count; goldens updated. Engine suite 43/43. Query cases 322/322 python, 264/264 typescript, 333/333 java; case property-setter-is-a-call fails on the base engine on all three write/delete checks. All six commits together, ten held-in repositories: test-selection recall 0.668 -> 0.700, precision 0.403 -> 0.452, impact source-caller recall 0.673 -> 0.686, path found 0.720 -> 0.752, context recall@5 0.789 -> 0.799; no repository regresses beyond 0.003 on any of them. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…operand of a 3+ way union counts Two gaps in "Optional[X] IS X", the rule that types a value annotated Optional[X] or X | None: - It existed for parameters, returns and fields but not for a LOCAL variable's annotation, so `ctx: Optional[Context] = ...` and `x: Foo | None = ...` left every call on the local unresolved while the same annotation on a parameter resolved. - `|` is left-associative: `A | B | None` is `(A | B) | None`, so A and B sit two levels below the annotation, under a nested union, and the depth-1 element rule saw only that nested union (which names no type) and None. Every union of three or more operands lost its members, on parameters, returns, fields and locals alike. binding_declared_nominal gains the Optional/union clause; union_operand walks nested PEP 604 unions and feeds their operands to type_ref_element, resolved and by name. Containers are untouched (union_operand is rooted at an Optional/union kind only). Optional/union locals occur in 15 of 16 public repositories measured (96 written with `|`, 50 with Optional[]/Union[]). Ten held-in repositories, against the previous commit: path found 0.752 -> 0.756, impact source-caller recall 0.686 -> 0.687 (one repository 0.795 -> 0.807); the new callers are a union's dispatch set and are labelled `one of a set`, resolved-caller precision unchanged at 0.979. Engine suite 43/43, torture unchanged; query cases 325/325; case optional-and-union-annotations fails on the base engine on both the local and the three-operand checks. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… a test file imports the conftests above it The import facts named a Python module only by its path from the repository root, so in a src layout `src/app/core.py` was `src.app.core` and no `import app.core` matched it: a src-layout repository had almost no test-to-source imports, which the import walk (a function that runs while a module is imported breaks every file that imports it) and the loads-the-change filter both read. A module is now also named from its package root, the first directory above it that is not a package. pytest imports every conftest.py from the rootdir down to a test file's directory before the file, so what a conftest imports is imported for each test file beneath it; a test file now imports those conftests. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`namespace Shop.Tests; using Results;` imports Shop.Results, and `Reflection.Info.Create()` written inside namespace Shop names Shop.Reflection.Info: C# reads the first segment of a using's name, and of a qualified name, from the enclosing namespaces outward. The engine took both as written, so the using imported nothing and every client type named through it read as a library type (`new Failure(..)` was boundary_lib), and the qualified receiver stayed untyped. - module_in_scope: a using whose name is no namespace at all takes the child of the file's namespace chain that does exist (the IR does not say whether the using sits inside the namespace; a name that is no namespace only compiles there). - type_name_cand: a type's qualified name is split at each dot into namespace prefix and dotted tail; the tail is a rank-1 candidate in every file whose namespace chain holds the prefix. Keyed from the declaration side. Case a-namespace-name-is-read-from-its-namespace fails 4/6 on the base (the two controls, a file in an unrelated namespace, pass on both). Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`axiomcode path Tests/OrderTests.cs Order.Place` was refused with "nothing named 'Tests/OrderTests.cs' is declared" on every C# graph, although the file is indexed: the file-endpoint branch matched .ts/.tsx/.js/.mjs/.cjs/.py/.java only. Every extension a front end reads (ax_langs.BY_EXT) is now a file endpoint. An extension outside the old list falls through to the name lookup when no indexed file has that name, so `Owner.cs` still resolves a method `cs` of Owner (the case's control). Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`using (var w = new Writer(..)) { w.Write(..); }` and
`using var w = new Writer();` left the receiver untyped: var_value_initializer
read LOCAL and CONST declarations only, and the IR gives these the
declarationKind USING. The resource is the disposable itself, so its
initializer is a value of its type exactly as a local's is.
Case a-using-resource-is-typed-by-its-initializer: both shapes fail on the base;
the control (a foreach variable is typed by its element, never by the
collection) passes on both.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
A constructor with no `: base(..)` / `: this(..)` runs `base()` before its body, and a class that declares no constructor gets an implicit one that does the same. Neither is written, so the base constructor had no caller: an abstract base whose constructor every derived class runs read as uncalled. New synthesised call_edges kind implicit_base_ctor (known_edge): - from a declared constructor without an initializer (and not a primary constructor whose heritage passes arguments), FromExpr = the base's heritage type reference, as primary_ctor_base does; - from a `new T()` site where T declares no constructor, beside its known_implicit_ctor row. The target is the base's constructor that takes no argument, walking up through bases that declare none. Registered in the bundle schema and ax_edges' kind vocabulary (`ctor`). Case a-constructor-runs-its-base-constructor: 4 positives fail on the base; the controls (`: base(name)` and `: this(..)` constructors gain no implicit edge) pass on both. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`var services = new ServiceCollection(); services.AddWidgets();` and `Configure(s => s.AddWidgets())` with `Configure(Action<IServiceCollection> c)` never reached the client's `AddWidgets(this IServiceCollection s)`: both receivers' types are unstaged, so they have no group, and call_recv_type_name -- the NAME the by-name rung (3) and the loose DI rung (7, #1472) compare -- had no clause for either. call_recv_type_name now also gives - an implicitly typed local initialised with `new T(..)`: the T written there; - an implicitly typed lambda parameter: the name of the delegate argument that types it (lambda_param_arg_ref, the same reference param_type reads). So the lambda's call matches by name (resolved) and the local's is a rung-7 candidate (one of a set, keeping its external label). Case an-unstaged-receiver-keeps-its-type-name: both positives fail on the base; controls (an extension on an in-source `this` type, a lambda typed as another unstaged type) pass on both; extension-on-unstaged-receiver unchanged. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`var f = Context.Current.Factory; f.Create()` -- the getter of the static property `Current` was an edge, but its result had no type: expr_type for a MEMBER_ACCESS needed an expr_type on the qualifier, and a qualifier that names a TYPE has none. So `.Factory` and every call after it were unresolved. Two clauses mirror the instance ones with ref_names_type on the qualifier (property and field). ref_names_type already refuses a name that also binds a value, the language's member-over-type rule. Case a-static-member-read-types-the-chain: four shapes (local, one chain, a var of the static read, a static field) fail on the base; the "Color Color" control -- a property named like the type is the value read -- passes on both. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`Cache<int>.Get("k")`, `Registry<A, B>.Create()` and
`Pair<int, string>.Make().Use()` were unresolved: the parser emitted a
generic_name in expression position with typeArgumentCount set and NO
potentialQualifiedName, so the receiver had no name to resolve.
- parser: a generic_name row carries its identifier as the written name (its
arity stays in typeArgumentCount).
- engine: expr_type_argc projects the count; ref_names_type and the call
receiver's type_ref_demand resolve the name at that arity, so `Cache<int>`
names Cache`1 and never a non-generic Cache. Every receiver written without
type arguments has arity "0", which is what both clauses used before.
Case a-generic-type-name-is-a-static-receiver: three positives fail on the
base; the non-generic `Cache.Get` control resolves to its own type on both.
Parser C# suite 62/62.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
A call written in a field initializer has no method around it, so the engine names the type as its caller, and a static or instance block is a <clinit> / <init_block> callable. Nothing calls either, so the upward walk stopped there: a factory whose `INSTANCE = new Factory()` throws breaks every test that touches the factory, and none of them was selected. impact.dl now hops from such an initializer (a type reached as a caller, or a <clinit> / <init_block>) to whoever uses the type in a way that runs it: a call to one of its members, a construction of a subtype, a read or write of one of its fields. The hop is taken only where the walk arrives at the type as a caller, never from a type or field target's own seed. Test routes through it carry a new rung, `at load`, ranked beside `at import`. The SQL fast path declines a closure that reaches such an initializer. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
`static final F X = // why
new F() { ... };` — the Java field extractor took the node right after `=` as the
initializer, and a comment is a named node in tree-sitter, so the whole initializer was
dropped: its calls, and an anonymous class it creates together with that class's methods.
A try-with-resources declaration had the same read. Both now skip comments, as a local
variable's initializer already did.
The comment-invariance twins gain a field initializer, a field holding an anonymous class
and a resource declaration; the case checks that a test reaching the anonymous class's
method through its interface is selected, beside the same shape with no comment.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
A method query is handed the field and field-access facts empty (KIND_SKIPS: only a field or type target was thought to reach them), so the `at load` hop's field clause derived nothing and a test that touches the initialized type only by reading one of its static fields (`Factory.INSTANCE_NULL.lookup(...)`) was dropped. Both relations are now loaded for the kinds that walk the closure; every other rule that joins them also needs a relation that stays skipped for those kinds, so nothing else changes. The case gains that shape. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…lds it `equals`, `hashCode` and `toString` are run by a HashMap key, a StringBuilder, String.format, an assertion; an override of a library method (`iterator` of an Iterable, `size` of a Collection, `close`) by the for-each loop, the collection, try-with-resources. The call is written on a library receiver or not at all, and Object's members are deliberately not fanned to client overrides by the engine, so a change to one had no caller and selected no test. The protocol hop Python already has (whoever constructs the type runs its protocol members) now has Java clauses: a member named equals / hashCode / toString, or one that overrides a library method, is reached by the callers of the type's constructors and of its subtypes' constructors. Still taken only from the change itself, under the `protocol` rung. The SQL fast path declines such a target. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…it resolves to the .py The project linker keys modules by qualified name, and a package that ships `impl.pyi` beside `impl.py` declares that name twice. The later file won the map, and `.pyi` sorts after `.py`, so `from .impl import *` and `import pkg` resolved to the stub. The engine refuses a stub declaration as an edge target, so every call into such a package ended at the library boundary and its tests reached nothing in it. A stub no longer replaces a non-stub module of the same name. A stub with no `.py` beside it is still the import's target. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
The spawn link (a test that starts a process whose arguments name an indexed file) only read PATHS. `subprocess.run([sys.executable, "-m", "pkg", ...])` names a MODULE: Python runs pkg/__main__.py (or pkg/mod.py for `-m pkg.mod`) as __main__, so everything the package's entry reaches was untested from that test's point of view, and a CLI suite that drives its tool this way selected nothing for an edit to the tool's internals. ax_spawn now resolves the name after each `-m` like an import: from the repository root or a source root (a directory that is no package and no test directory), package __main__ first, and only to a file this graph indexed. `-m coverage run -m pkg` names two modules; coverage is not indexed and links nothing. A string that only mentions `-m pkg` starts no process and stays unlinked. IMPACT_VERSION 66 -> 67 (spawns_fact changed). Measured with the mutation oracle on sixteen public repositories: the one whose CLI tests spawn `-m <tool>` moves test-selection recall 0.422 -> 0.468 (+175 failing test files selected, none lost, 121 added files that do not fail); a second repository's `python -m <pkg>` spawn links but its targets sit behind a protocol call, so it does not move; no other repository spawns `-m`. Case test-runs-a-module-with-dash-m fails 3 of 4 checks on the base. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…members of self's constructed subclasses
Visitor and parser dispatch by a computed name -- `return getattr(self,
f"visit_{type(node).__name__}", None)` in a base class, then `f = self.get_visitor(node);
f(node)` -- had no edges at all, so every visit_* body was cut off from whoever drives the
visitor. Three gaps stacked:
- the parser gave an f-string an empty literal value. It now carries the f-string's
constant lead (the text before its first `{`), which every string it can produce starts
with; nothing else reads an FSTRING literal.
- the existing prefix rule (`getattr(obj, "as_" + x)`) read only a `+` concatenation and only
obj's own MRO. It now also reads an f-string lead, and on `self` it widens over the
CONSTRUCTED subclasses, the same RTA bound self.m() has; a decorated member resolves to
the wrapper its decorator returned.
- a method returning such a lookup hands the set back (method_returns_method), as it would a
bare name.
The set is the program's own dispatch table, so the dispatch cap no longer refuses it
(call_computed_name_family): a visitor with sixty members is sixty targets by
construction, and the cap had dropped the whole site.
Mutation oracle, sixteen public repositories (ten tuning, six held out): the visitor-heavy
tuning repository moves test-selection recall 0.474 -> 0.878 (+169 failing test files
selected, none lost; 203 added files do not fail, the same 0.45 precision as its base);
one held-out repository +2; every other repository unchanged. Case
getattr-fstring-prefix-dispatch fails 3 of 4 checks on the base (2 of 4 with the f-string
rule but without the cap exemption). value-callee-is-unknown's computed-name check moves
to a name with no written part (`getattr(self, event)`), which stays an unknown: the
f-string form it used now resolves, as its `+` control already did.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…elf without an annotation with_target_declared typed a `with ... as` target from __enter__'s declared return or a literal `Self`. An unannotated `def __enter__(self): return self` -- the commonest way to write it -- typed nothing, so `with Session() as s: s.request(...)` was an untyped receiver and everything the session does was cut off from the code that opens one. Five such __enter__ definitions in three of sixteen public repositories. The written return is now read: no annotation and a return value that is `self` binds the target to the context manager's own type. An __enter__ that returns something else is untouched (control in the case). Mutation oracle, sixteen repositories: one tuning repository +22 failing test files selected (14 added files do not fail), none lost; all others unchanged. Case with-enter-returns-self fails its first check on the base. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… its targets from the annotation `for name, field in self.fields.items()` with `fields: dict[str, Field]` bound nothing: no element rule looked through a view call, and an items() loop has a TUPLE target that no rule bound. About 80 loops over an annotated mapping's view in eleven of sixteen public repositories. mapping_annotation_of finds the container annotation an expression carries (a field through self or a typed object, a parameter, a callee's declared return, a single-write local holding one); the slots are read POSITIONALLY (key at generic position 0, value at 1) rather than through type_ref_element, which merges both. values()/keys() yield one slot; items() binds each name of a tuple target to its own slot. The view names are catalogued in builtins.dl. Mutation oracle, sixteen repositories: one tuning repository +11 failing test files selected (7 added do not fail), none lost; path found on tuning 0.756 -> 0.764. Case dict-view-loop-types-the-value fails 3 of 4 checks on the base; its control (a key slot is never the value type) passes on both. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… argument the runner fills
`@pytest.mark.parametrize("cls", [String, Integer])` calls the test once per value, and
`@pytest.fixture(params=(A, B))` hands `request.param` each one in turn; neither is a
written call, so the parameter stayed untyped. Parametrize values that are classes,
instances or functions occur in fourteen of sixteen public repositories.
framework-behavior/dispatch.dl reads the rows: the decorator's values written in place, or
a display held by a single-write module name (also through `from m import NAME`); one
argument name takes the whole row, several take the row's k-th element (the existing
argnames position, now asked for every direct argument); `pytest.param(...)` rows count.
value-flow.dl types the parameter from each value (instance, class object, or function),
and `request.param` inside a params fixture likewise; a fixture that returns a class makes
the requesting parameter that class. The pytest names are catalogued in knobs.dl.
Mutation oracle, sixteen repositories: one held-out repository +17 failing test files
selected, another +1; 154 added files do not fail (the converter fixture there types every
test that takes it; 52 of the 169 added ran the target). Nothing lost. Case
parametrize-values-type-the-argument fails 3 of 4 on the base; control: string values
naming a class type nothing.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…s run the argument's dunder `assert repr(v) == "<...>"` reached nothing of v's: the call resolves to the builtin (C code) and the dunder it runs had no caller. BUILTIN_PROTOCOL edges now go from the argument expression to the dunder repr, str, len, hash, bool, iter, next, abs, format and reversed run on the argument's type -- only for the bare builtin (a client `repr` is not it), with exactly one argument (`str(b, "utf-8")` decodes), and with CPython's fallbacks (str -> __repr__, bool -> __len__ when the class has no slot of its own). New call kind BUILTIN_PROTOCOL in the schema vocabulary, tiered by target count like the other protocol edges; the names are catalogued in builtins.dl. Mutation oracle, sixteen repositories: +13 failing test files selected across five repositories and NO added file that does not fail; path found 0.764 -> 0.776 on tuning and 0.600 -> 0.607 held out. Case builtin-runs-the-dunder fails 3 of 4 on the base; control: str() on a class with its own __str__ does not run its __repr__. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…rgument does `def getter(val, field_obj=field_obj): return field_obj.deserialize(val)` -- the closure idiom that pins a loop variable -- is called with one argument, so field_obj only ever holds its default, and no call site passes it: the parameter was untyped and the call on it unresolved. The default is a value the def statement writes once (the parser links it, param_default_expr); value-flow.dl now lets it reach the parameter exactly as a call-site argument does -- an instance types it, a class object makes it that class, a function makes it callable as that function -- alongside whatever callers pass. A parameter defaulted to a name from an enclosing scope appears in fifteen of sixteen public repositories (sentinels and constants included, which type nothing callable). Mutation oracle, sixteen repositories: one tuning repository +13 failing test files selected (16 added files do not fail), none lost, every other repository unchanged. Case parameter-default-types-it fails 2 of 3 on the base; control: an argument passed at a call site still reaches the parameter. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…o it only where the module sits under the dropped segments findModule dropped leading segments of an import it could not match exactly and then matched what was left exactly or by suffix. `from clikit.testing import CliRunner` dropped `clikit` and found the project's own `testing` package (or `app.testing` by suffix); `from fw.db import models` found the project's `models.py`. A dependency's module whose last segments collide with one of the project's resolved INTO the project, the import bound nothing, and every call through it was unresolved instead of a named boundary. The drop exists for an import more qualified than the module (`import myproject.framework` naming a `framework` in a `myproject/` directory with no __init__.py). The analyzer now records the directories above each module's top package, and a dropped prefix must be what the module actually sits under. Python engine goldens: 33-url-view-forms, 37-model-signals and 38-app-wiring move sites from ambiguous_unknown to boundary_lib (`external:path`, `external:models.CharField`, `external:call_command`); edges, framework edges and the torture harness are otherwise unchanged. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… hands back to the objects the client gives it The engine reads a dependency's signatures, never its bodies, so a hop the LIBRARY makes back into client code was invisible: a test client's get() runs the open() a client subclass overrides and then calls the application it was built around; a CLI runner's invoke(cli) runs cli.main(), which runs the command's callback; a decorator wraps a def into a library object that later calls it; ExitStack.enter_context(cm) runs cm.__enter__. On a 16-repository Python oracle a library frame was the first broken hop behind most test-impact misses of the web-framework subject, and every one of them stopped at the library. graph/python/libsum/libsum.py derives the hand-backs from the library's SOURCE (the project's own environment, its stdlib through pyvenv.cfg, or AXIOMCODE_PY_SITE): a flow-insensitive pass per function records which parameter-rooted value meets which call or protocol syntax, composed to a fixpoint across the library's own calls (self-calls, typed parameters and fields, constructors, closures, decorator factories). It writes, for the library classes and functions the client can name, five tables beside the client IR: the hand-backs (callee, root, position, path, member, via, declared type), constructor stores, re-exports, what a decorator turns a def into, and class ancestors. Cached by the installed distributions and the client's imports, so a refresh that adds no import reuses them. Staged through a new client-extra.map (facts beside the IR that are not parser schema); absent = empty = no change. framework-behavior/library-callbacks.dl joins them to the client: a member a client type inherits from its library base, a super() past the client classes, a library function or class the client imported, a member of a library object the client built (followed through locals, helpers' returns and fixtures) or one a library decorator made of a def. Each hand-back resolves to the client member of the object that arrives -- the receiver, an argument, a field the library constructor filled, a field the client set -- and only when none answers, to the client subclasses of the type the library declares there. It ADDS lib_callback_edge (caller, target, callee, member, via, how) and lib_callback_site, and leaves call_chain_edge alone. No library or framework is named in a rule. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
… hop `library callback` impact reads the Python engine's lib_callback_edge as fw_edge "library callback": a test that drives a library now reaches the client member the library hands back to, with the rung `library callback` (ranked with `registered`: a library's behaviour joined by a member name, never a resolved call). lib_callback_site marks a call site the engine knows enters a library callable as a library receiver, so it is never a by-name seed: a test client's `client.put()` no longer reaches the project's own `put` by name. tests/cases/python/library-calls-back: a fake site-packages with a test client, a CLI runner and command decorator, a template environment whose loader arrives through **options and a function calling a member on its argument; each positive check fails on the base engine. Controls: the same member name on a class never handed to a library, an object handed to a library function that never calls that member, a same-named member on a class that is not a subclass of the declared library type. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
…ependent impact names
path adds lib_callback_edge to the hops no call site expresses, worded `library_callback via <callee> -> <member>`
and verified against the engine's table, so a test reaches the application its test client calls; impact lists
the client method that calls the library as a [framework] dependent of the member handed back
("a library it calls hands this back (<callee> calls <member>)"), which is what path's note points at.
Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
# Conflicts: # graph/python/souffle/decls_all.dl # parser/src/parsers/python/extractors/python-resolution-linker.ts # parser/src/workflows/python/python-project-analyzer.ts # plugins/axiomcode/skills/axiomcode/scripts/axiomcode-impact
… the f-string golden A Python call site with more candidate targets than the cap was refused outright, so every test reached only through it was dropped from a selection. For test selection a missed test costs more than an extra one, so Python solves uncapped unless --dispatch-cap is given; every other language keeps 20. The protocol-fan check tests the cap mechanism itself and now pins it explicitly. The parser's Python golden records an f-string's leading text (literalValue), as the getattr dispatch change made it do; only that column moved, on 14 rows. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com>
swapnilpaliwal-sd
requested review from
JaredHLZhang,
Whua689 and
suyashpaliwal26
as code owners
October 10, 2026 01:26
swapnilpaliwal-sd
changed the base branch from
release/0.1.9-impact-loops
to
apps/integration-0.1.9
October 10, 2026 03:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Python test-selection recall, on top of #1882 (based on its branch so the diff shows only this). Merge #1882 first, then this; it supersedes #1883 and #1884, which are merged in here with their conflicts resolved.
What it adds
library callbackedges. No library is named in any rule; cached per installed distribution; no environment means no rows and unchanged behaviour. Includes a parser fix: an import whose last segment matches a project module (x.testing) no longer resolves into the project.python -m pkgspawns reachpkg/__main__.py;getattr(self, f"visit_{…}")dispatch (the parser records an f-string's leading text);with X() as ythrough an untyped__enter__returning self;.items()/.values()/.keys()loop variables;@parametrizeand fixtureparams=typing;repr/len/str/…builtins reaching the argument's dunder; a parameter typed by its default value.--dispatch-capstill overrides.literalValuemoved, 14 rows); the cap-mechanism check pins its cap explicitly.Measured (16 public repositories, mutation oracle; base = #1882):
No repository loses recall; source-caller recall and path found rise on several and fall on none. One held-out repository is not a clean held-out number: the parser defect in (1) was found while inspecting it.
Checks: query cases python 366/366, typescript 297/297, javascript 319/319, java 343/343, csharp 241/241; Python engine suite 43/43 (torture unchanged); parser Python suite 23/23. First index costs 3–9 s more where library summaries are extracted; warm index unchanged.