From 80430553fb9cd01270cf03021758baecac66563d Mon Sep 17 00:00:00 2001 From: Swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Wed, 14 Oct 2026 21:26:17 -0700 Subject: [PATCH] python: a method handed over as a value reaches its target A bound or unbound method or a function named as a VALUE and not called (executor.submit(self._work, x), Thread(target=svc.run), sorted(xs, key=self.rank), [h.on_event], partial(svc.cancel, oid), view_func= views.index, return self._handle) had no caller in the Python graph, so impact OrderService.cancel did not name the function that queued it and path said the two were independent. The engine already resolved what each of these expressions denotes; it dropped the answer because no call wrapped it. call-edge-generation/method-reference.dl emits it as a call_chain_edge with kind METHOD_REFERENCE and tier callback_registered, the tier the other front ends use for a function handed over, so answers say "handed over as a value" and never "calls it". Only what is resolved: a typed receiver through the MRO, a class's unbound method, a scoped name, a module member. Refused: an untyped receiver (stays as unresolved as thing.cancel() is), a flowed value (a parameter, a name or attribute assigned from an expression), a property read, a decorator application, the argument of a one-dunder builtin (str(f)), and a comparison operand. Measured on three open-source Python projects used for tuning: 3-4% more resolved caller->callee pairs, and 20 of 20 sampled new edges right after the guards (17 of 20 before the flowed-value guard). On three held-out projects, graded blind by a reviewer who did not tune it: 39 of 40 sampled new edges right (Wilson 95% interval 0.87-1.00). The one miss is a function handed to an unregistration call; the only repeated wrong shape left is a function passed to inspect.signature, about 1% of the new edges. The engine goldens gain only reference rows; the torture oracle scorecard is unchanged. Refs #1832 --- graph/bundle/SCHEMA.md | 6 +- graph/bundle/schema.ts | 6 +- .../call-edge-generation/method-reference.dl | 123 ++++++++++ .../test/python/expected/01-plain-calls.edges | 1 + .../test/python/expected/01-plain-calls.tiers | 8 +- .../test/python/expected/07-decorators.edges | 5 + .../test/python/expected/07-decorators.tiers | 8 +- graph/test/python/expected/08-callables.edges | 2 + graph/test/python/expected/08-callables.tiers | 8 +- .../python/expected/09-args-forwarding.edges | 1 + .../python/expected/09-args-forwarding.tiers | 8 +- .../test/python/expected/12-blind-spots.edges | 1 + .../test/python/expected/12-blind-spots.tiers | 8 +- .../expected/17-rebound-metaclass-slot.edges | 1 + .../expected/17-rebound-metaclass-slot.tiers | 8 +- .../expected/21-url-and-signal-dispatch.edges | 7 + .../expected/21-url-and-signal-dispatch.tiers | 8 +- .../python/expected/22-di-providers.edges | 6 + .../python/expected/22-di-providers.tiers | 8 +- .../25-imported-decorated-names.edges | 2 + .../25-imported-decorated-names.tiers | 8 +- .../expected/27-returned-expression.edges | 3 + .../expected/27-returned-expression.tiers | 8 +- .../python/expected/28-assigned-members.edges | 2 + .../python/expected/28-assigned-members.tiers | 8 +- .../python/expected/33-url-view-forms.edges | 10 + .../python/expected/33-url-view-forms.tiers | 8 +- .../36-call-through-model-decorators.edges | 1 + .../36-call-through-model-decorators.tiers | 8 +- .../python/expected/37-model-signals.edges | 1 + .../python/expected/37-model-signals.tiers | 8 +- .../test/python/expected/38-app-wiring.edges | 6 + .../test/python/expected/38-app-wiring.tiers | 8 +- .../39-model-construction-hooks.edges | 2 + .../39-model-construction-hooks.tiers | 8 +- .../python/expected/41-di-marker-forms.edges | 8 + .../python/expected/41-di-marker-forms.tiers | 8 +- .../python/expected/42-signal-forms.edges | 1 + .../python/expected/42-signal-forms.tiers | 8 +- .../expected/project-name-collision.tiers | 8 +- .../project-two-service-fastapi.tiers | 8 +- graph/test/python/tools/literal_gate.py | 6 +- .../python/torture/expected/torture.edges | 5 + .../skills/axiomcode/scripts/ax_edges.py | 2 +- .../case.json | 230 ++++++++++++++++++ .../orders.py | 163 +++++++++++++ .../tests/test_ledger.py | 5 + .../tests/test_queue.py | 9 + 48 files changed, 708 insertions(+), 67 deletions(-) create mode 100644 graph/python/engine/call-edge-generation/method-reference.dl create mode 100644 tests/cases/python/method-reference-reaches-its-target/case.json create mode 100644 tests/cases/python/method-reference-reaches-its-target/orders.py create mode 100644 tests/cases/python/method-reference-reaches-its-target/tests/test_ledger.py create mode 100644 tests/cases/python/method-reference-reaches-its-target/tests/test_queue.py diff --git a/graph/bundle/SCHEMA.md b/graph/bundle/SCHEMA.md index 6b33b221c..6cd2c4bd2 100644 --- a/graph/bundle/SCHEMA.md +++ b/graph/bundle/SCHEMA.md @@ -442,7 +442,7 @@ One row per place a call is written (or, for a synthesised edge, the construct t - **typescript** — end_line / end_column come from the expression row; the call-site row itself records only the start. - **javascript** — caller_id is the parser's enclosing method, or the module initializer for top-level code. end_line / end_column come from the expression row. `require()` is a module edge, not a call site. - **typescript** — PROPERTY_READ and PROPERTY_WRITE rows are accessor invocations with no written call: the site is the property-access expression that runs the getter or setter, positioned from the expressions table, and callee_name is NULL because nothing was written; the accessor's name is on the callee's methods row. Filter them out with kind NOT IN (…) when counting calls. -- **python** — PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, BUILTIN_PROTOCOL, OPERATOR_PROTOCOL, TRUTH_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls. +- **python** — PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, BUILTIN_PROTOCOL, OPERATOR_PROTOCOL, TRUTH_PROTOCOL, METHOD_REFERENCE, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls. - **python** — The id is an EXPRESSION hash for a written call; a DECORATOR hash (PY_DECORATOR_…) for DECORATOR_APPLICATION and DECORATOR_* sites, positioned at the decorator line; and the class's TYPE hash for METACLASS_CREATION, positioned at the class declaration. ### `call_edges` @@ -541,6 +541,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `SUBSCRIPT_PROTOCOL` | python | `x[k]` runs `__getitem__` (and `x[k] = v` / `del x[k]` the setter and deleter) of the receiver's class. No written call; the site is the subscript expression. Its own kind so it is never counted as a written call. | | `BUILTIN_PROTOCOL` | python | `repr(x)`, `str(x)`, `len(x)`, `hash(x)`, `bool(x)`, `iter(x)`, `next(x)`, `abs(x)`, `format(x)` and `reversed(x)` run the matching dunder of the argument's class (str falls back to `__repr__`, bool to `__len__`). The written call is to the builtin; this edge is the dunder it runs. The site is the argument expression. Its own kind so it is never counted as a written call. | | `OPERATOR_PROTOCOL` | python | An operator runs a dunder of its operands: `a + b` the left operand's `__add__` and the right one's `__radd__`, a comparison the left slot and the right one's mirror (`a < b` -> `b.__gt__`, `!=` falls back to `__eq__`), `x in c` the right operand's `__contains__` (or `__iter__`), `-a`/`+a`/`~a` its `__neg__`/`__pos__`/`__invert__`. No written call; the site is the operator expression. | +| `METHOD_REFERENCE` | python | A method or function READ AS A VALUE, not called: an argument (`pool.submit(self.work, x)`, `map(Cls.convert, rows)`, `partial(svc.cancel, oid)`), a keyword (`Thread(target=svc.run)`), an element, a stored or returned value. The target is what the expression resolves to (a typed receiver through the MRO, a class, a scoped name, a module member); an untyped receiver gives no row. Always tier callback_registered; the site is the reference expression (call-edge-generation/method-reference.dl). | | `TRUTH_PROTOCOL` | python | A truth test runs the tested value's `__bool__`, or `__len__` when its class has none: `if x:`, `while x:`, `assert x`, `not x`, a conditional or comprehension condition, and an operand of `and`/`or` (the right one only when the whole expression is tested). No written call; the site is the tested expression. | **`call_edges.tier` values** @@ -564,6 +565,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h | `dynamic_terminal` | javascript | `obj[expr]()`, `eval`, `import()`: no static target by construction; callee is NULL. | | `fan_capped` | javascript, java, csharp | More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count. | | `callback_registered` | javascript, typescript | The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on('x', h)`, `setTimeout(f)`), which may invoke it. Not the site's own callee; a reachability edge, labelled so it is never read as a resolved call. | +| `callback_registered` | python | A method or function named as a VALUE and handed over, stored or returned (`pool.submit(self.work)`, `Thread(target=svc.run)`, `sorted(xs, key=self.rank)`, `[h.on_event]`), always kind METHOD_REFERENCE. Its target is resolved the way a call on the same receiver would be; whoever receives the value calls it. A reachability edge, labelled so it is never read as a resolved call. | | `event_dispatch` | javascript | `x.emit('name')` reaching a handler registered by `x.on('name', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one. | | `event_dispatch` | typescript | A publish on a project class's keyed registry reaching each function filed under the same key: `bus.publish('x', p)` where `publish` (or a method it passes the key to) calls what `this.handlers.get(name)` holds, to the function a `bus.subscribe('x', f)` stored there, or an entry `{ ['x']: f }` of a table a subscribe-all method files by key. A literal, a const or a const-object member key; a key only known at run time matches every key, and a constant the dispatch also looks up (a wildcard) matches every publish. The site is the publish call (call-edge-generation/keyed_registry.dl). | | `event_dispatch` | java | A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e's static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl). Also a JPA entity write (`save`, `persist`, `merge`, `delete` on a repository or EntityManager) reaching the `@PrePersist`/`@PreUpdate`/`@PreRemove`-style callbacks of the written entity's type and of the listeners `@EntityListeners` names on it or a superclass (kind entity_callback; call-edge-generation/entity_lifecycle.dl). | @@ -586,7 +588,7 @@ THE GRAPH. One row per (site, resolved target). A site with N possible targets h - **python** — A `boundary_lib` edge may point at a builtin (callee_provenance builtin, callee_label `builtin:NAME`) or at an unstaged import path (callee_provenance external) — neither has a methods row. - **java** — A `boundary_lib` edge with callee_provenance external names a method of an ancestor type no staged IR declares (callee_label `external:.`, no methods row). A site whose receiver is declared as such a type is multi_inferred even with one client override: the platform method itself, and the platform's own subclasses, are the other possible targets. Stage the library to replace the label with the real method. - **python** — The reason a site is ambiguous_unknown is exported per site in ext_call_site_unresolved (site, caller, reason, detail). -- **all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript and TypeScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list. +- **all** — THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript and TypeScript, callback_registered (kind METHOD_REFERENCE) in Python; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits the four shared tiers and callback_registered. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list. - **java** — A multi_inferred fan is CHA-wide: it is every override the hierarchy admits, bounded only by the dispatch cap. type_instantiated is computed and exported but NOT read by any rule, so the fan is not narrowed to types the program constructs. Narrow it yourself by joining dispatch_candidates to type_instantiated — see the dispatch_envelope_of query. A receiver is ALSO typed by what flows into it (a local's initializer, the arguments callers pass to a parameter, the receivers callers invoke a method on for its `this`), and each flow-in type resolves its member directly, outside the fan: that is why a `fan_capped` site still carries edges, and why they are the types the program was seen to hand over, not the whole hierarchy. - **typescript** — A multi_inferred fan is CHA-wide, as in Java: type_instantiated is computed and exported but NOT read by any rule. The fan also has sources that are not virtual dispatch at all — an overload set or a union-typed receiver produces one too. - **python** — A multi_inferred fan IS narrowed by the instantiation set: type_instantiated_reachable (the constructed classes and their bases) bounds dispatch in resolution/dispatch.dl. Python is the only front end where that narrowing is applied, so a fan here is tighter than the same shape would be in Java or TypeScript. diff --git a/graph/bundle/schema.ts b/graph/bundle/schema.ts index 731fb61b6..9d71c3906 100644 --- a/graph/bundle/schema.ts +++ b/graph/bundle/schema.ts @@ -524,6 +524,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'tier', value: 'dynamic_terminal', languages: S, meaning: '`obj[expr]()`, `eval`, `import()`: no static target by construction; callee is NULL.' }, { table: 'call_edges', column: 'tier', value: 'fan_capped', languages: ['javascript', 'java', 'csharp'], meaning: 'More targets than --dispatch-cap: the set was refused rather than emitted. JavaScript: callee is NULL. Java and C#: callee is the declared base method the fan would have started from; dispatch-capped-sites.csv carries the refused count.' }, { table: 'call_edges', column: 'tier', value: 'callback_registered', languages: ['javascript', 'typescript'], meaning: 'The site HANDS the callee this function (`xs.forEach(f)`, `p.then(f)`, `emitter.on(\'x\', h)`, `setTimeout(f)`), which may invoke it. Not the site\'s own callee; a reachability edge, labelled so it is never read as a resolved call.' }, + { table: 'call_edges', column: 'tier', value: 'callback_registered', languages: P, meaning: 'A method or function named as a VALUE and handed over, stored or returned (`pool.submit(self.work)`, `Thread(target=svc.run)`, `sorted(xs, key=self.rank)`, `[h.on_event]`), always kind METHOD_REFERENCE. Its target is resolved the way a call on the same receiver would be; whoever receives the value calls it. A reachability edge, labelled so it is never read as a resolved call.' }, { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: S, meaning: '`x.emit(\'name\')` reaching a handler registered by `x.on(\'name\', h)` on a value x may hold — name-sensitive for literal names, every handler on that value for a computed one.' }, { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: T, meaning: 'A publish on a project class\'s keyed registry reaching each function filed under the same key: `bus.publish(\'x\', p)` where `publish` (or a method it passes the key to) calls what `this.handlers.get(name)` holds, to the function a `bus.subscribe(\'x\', f)` stored there, or an entry `{ [\'x\']: f }` of a table a subscribe-all method files by key. A literal, a const or a const-object member key; a key only known at run time matches every key, and a constant the dispatch also looks up (a wildcard) matches every publish. The site is the publish call (call-edge-generation/keyed_registry.dl).' }, { table: 'call_edges', column: 'tier', value: 'event_dispatch', languages: J, meaning: 'A Spring application event: `publishEvent(e)` reaching each listener (`@EventListener`, `@TransactionalEventListener`, `ApplicationListener.onApplicationEvent`) whose declared event type e\'s static type is, or is a subtype of. Added beside the publishEvent boundary row, never in place of it (call-edge-generation/event_dispatch.dl). Also a JPA entity write (`save`, `persist`, `merge`, `delete` on a repository or EntityManager) reaching the `@PrePersist`/`@PreUpdate`/`@PreRemove`-style callbacks of the written entity\'s type and of the listeners `@EntityListeners` names on it or a superclass (kind entity_callback; call-edge-generation/entity_lifecycle.dl).' }, @@ -684,6 +685,7 @@ export const VOCAB: readonly VocabSpec[] = [ { table: 'call_edges', column: 'kind', value: 'SUBSCRIPT_PROTOCOL', languages: P, meaning: '`x[k]` runs `__getitem__` (and `x[k] = v` / `del x[k]` the setter and deleter) of the receiver\'s class. No written call; the site is the subscript expression. Its own kind so it is never counted as a written call.' }, { table: 'call_edges', column: 'kind', value: 'BUILTIN_PROTOCOL', languages: P, meaning: '`repr(x)`, `str(x)`, `len(x)`, `hash(x)`, `bool(x)`, `iter(x)`, `next(x)`, `abs(x)`, `format(x)` and `reversed(x)` run the matching dunder of the argument\'s class (str falls back to `__repr__`, bool to `__len__`). The written call is to the builtin; this edge is the dunder it runs. The site is the argument expression. Its own kind so it is never counted as a written call.' }, { table: 'call_edges', column: 'kind', value: 'OPERATOR_PROTOCOL', languages: P, meaning: 'An operator runs a dunder of its operands: `a + b` the left operand\'s `__add__` and the right one\'s `__radd__`, a comparison the left slot and the right one\'s mirror (`a < b` -> `b.__gt__`, `!=` falls back to `__eq__`), `x in c` the right operand\'s `__contains__` (or `__iter__`), `-a`/`+a`/`~a` its `__neg__`/`__pos__`/`__invert__`. No written call; the site is the operator expression.' }, + { table: 'call_edges', column: 'kind', value: 'METHOD_REFERENCE', languages: P, meaning: 'A method or function READ AS A VALUE, not called: an argument (`pool.submit(self.work, x)`, `map(Cls.convert, rows)`, `partial(svc.cancel, oid)`), a keyword (`Thread(target=svc.run)`), an element, a stored or returned value. The target is what the expression resolves to (a typed receiver through the MRO, a class, a scoped name, a module member); an untyped receiver gives no row. Always tier callback_registered; the site is the reference expression (call-edge-generation/method-reference.dl).' }, { table: 'call_edges', column: 'kind', value: 'TRUTH_PROTOCOL', languages: P, meaning: 'A truth test runs the tested value\'s `__bool__`, or `__len__` when its class has none: `if x:`, `while x:`, `assert x`, `not x`, a conditional or comprehension condition, and an operand of `and`/`or` (the right one only when the whole expression is tested). No written call; the site is the tested expression.' }, // entry_points.reason @@ -763,7 +765,7 @@ export const NOTES: readonly NoteSpec[] = [ { language: 'typescript', table: 'overrides', note: 'EMPTY — this table is Java-shaped. The TypeScript dispatch envelope is in dispatch_candidates, with basis `nominal` or `structural`.' }, { language: 'typescript', table: 'type_instantiated', note: 'Every row has how = `new`. Not restricted to client provenance: a type the library constructs is still a type that exists at run time, and dropping it would narrow the envelope unsoundly.' }, { language: 'typescript', table: 'call_sites', note: 'PROPERTY_READ and PROPERTY_WRITE rows are accessor invocations with no written call: the site is the property-access expression that runs the getter or setter, positioned from the expressions table, and callee_name is NULL because nothing was written; the accessor\'s name is on the callee\'s methods row. Filter them out with kind NOT IN (…) when counting calls.' }, - { language: 'python', table: 'call_sites', note: 'PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, BUILTIN_PROTOCOL, OPERATOR_PROTOCOL, TRUTH_PROTOCOL, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.' }, + { language: 'python', table: 'call_sites', note: 'PROPERTY_READ, PROPERTY_WRITE, CONTEXT_MANAGER, ITERATION_PROTOCOL, BUILTIN_PROTOCOL, OPERATOR_PROTOCOL, TRUTH_PROTOCOL, METHOD_REFERENCE, METACLASS_CREATION and DYNAMIC_CALL rows are protocol or indirect edges with no written call: their site is the expression that triggers them, and callee_name is always NULL because nothing was written. SUBSCRIPT_CALL is NULL only when the subscript is not a written name (measured 206 of 337 rows on a Python subject). Filter them out with kind NOT IN (…) when counting calls.' }, { language: 'python', table: 'call_sites', note: 'The id is an EXPRESSION hash for a written call; a DECORATOR hash (PY_DECORATOR_…) for DECORATOR_APPLICATION and DECORATOR_* sites, positioned at the decorator line; and the class\'s TYPE hash for METACLASS_CREATION, positioned at the class declaration.' }, { language: 'python', table: 'call_edges', note: 'A `boundary_lib` edge may point at a builtin (callee_provenance builtin, callee_label `builtin:NAME`) or at an unstaged import path (callee_provenance external) — neither has a methods row.' }, { language: 'java', table: 'call_edges', note: 'A `boundary_lib` edge with callee_provenance external names a method of an ancestor type no staged IR declares (callee_label `external:.`, no methods row). A site whose receiver is declared as such a type is multi_inferred even with one client override: the platform method itself, and the platform\'s own subclasses, are the other possible targets. Stage the library to replace the label with the real method.' }, @@ -771,7 +773,7 @@ export const NOTES: readonly NoteSpec[] = [ { language: 'python', table: 'entry_points', note: 'Framework entry points only: url, http, orm_hook, task, signal_receiver, fixture, di_provider and grpc_service. There is no test and no main reason: a pytest test is recognised by the query layer from its file and name, not here.' }, { language: 'python', table: 'overrides', note: 'EMPTY — this table is Java-shaped. The Python dispatch envelope is in dispatch_candidates with basis `mro`, and `value` for a function assigned onto an instance\'s member; the raw linearisation is in ext_mro_position.' }, { language: 'python', table: 'type_instantiated', note: 'Every row has how = `new`: the rule set records that some client call constructs the class, not which form.' }, - { language: 'all', table: 'call_edges', note: 'THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript and TypeScript; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits only the four shared tiers. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.' }, + { language: 'all', table: 'call_edges', note: 'THE TRUST LINE, and it is not the same set of tiers in every language. RESOLVED (callee_method_id is set): known_edge and multi_inferred in every language, and boundary_lib where the library is staged (--library) — without it boundary_lib names the target in callee_label and leaves callee_method_id NULL; ALSO ambient_terminal in TypeScript, fan_capped in Java and C# (the declared base, the fan refused), and runtime_observed in C#. HANDED OVER (callee set, but the site passes the function rather than calling it): callback_registered and event_dispatch in JavaScript and TypeScript, callback_registered (kind METHOD_REFERENCE) in Python; event_dispatch in C# too, where a mediator Send or Publish runs the handler for the request type, beside the row for the site itself. CORRECT END (callee NULL, and nothing is missing): intrinsic_terminal in TypeScript; ambient_terminal, implicit_constructor and dynamic_terminal in JavaScript; known_implicit_ctor, known_builtin_operator and boundary_generated (callee_label set) in C#. BLIND SPOT (callee NULL; exactly the tiers named `ambiguous_*`, which are what unresolved_sites holds): ambiguous_unknown everywhere, ALSO ambiguous_anon in Java and ambiguous_dynamic in C#. CAPPED (callee NULL, not in unresolved_sites): fan_capped in JavaScript. Python emits the four shared tiers and callback_registered. A filter written as `tier IN (known_edge, multi_inferred)` therefore drops resolved edges in every language but Python — derive the set from this note or from unresolved_sites, never from a hardcoded list.' }, { language: 'java', table: 'call_edges', note: 'A multi_inferred fan is CHA-wide: it is every override the hierarchy admits, bounded only by the dispatch cap. type_instantiated is computed and exported but NOT read by any rule, so the fan is not narrowed to types the program constructs. Narrow it yourself by joining dispatch_candidates to type_instantiated — see the dispatch_envelope_of query. A receiver is ALSO typed by what flows into it (a local\'s initializer, the arguments callers pass to a parameter, the receivers callers invoke a method on for its `this`), and each flow-in type resolves its member directly, outside the fan: that is why a `fan_capped` site still carries edges, and why they are the types the program was seen to hand over, not the whole hierarchy.' }, { language: 'typescript', table: 'call_edges', note: 'A multi_inferred fan is CHA-wide, as in Java: type_instantiated is computed and exported but NOT read by any rule. The fan also has sources that are not virtual dispatch at all — an overload set or a union-typed receiver produces one too.' }, { language: 'python', table: 'call_edges', note: 'A multi_inferred fan IS narrowed by the instantiation set: type_instantiated_reachable (the constructed classes and their bases) bounds dispatch in resolution/dispatch.dl. Python is the only front end where that narrowing is applied, so a fan here is tighter than the same shape would be in Java or TypeScript.' }, diff --git a/graph/python/engine/call-edge-generation/method-reference.dl b/graph/python/engine/call-edge-generation/method-reference.dl new file mode 100644 index 000000000..c3bcf5da2 --- /dev/null +++ b/graph/python/engine/call-edge-generation/method-reference.dl @@ -0,0 +1,123 @@ +// ============================================================================ +// CALL-EDGE-GENERATION · METHOD REFERENCES (a callable named as a VALUE, not called) +// +// ── THE GAP ───────────────────────────────────────────────────────────────── +// executor.submit(self._do_work, item) threading.Thread(target=svc.run_job) +// loop.run_in_executor(None, svc.cancel, oid) sorted(xs, key=self._rank) +// callbacks = [handler.on_event] functools.partial(svc.cancel, oid) +// app.add_url_rule("/x", view_func=views.index) return self._handle +// Each hands a method or function over as a value; whoever receives it calls it, later, from +// a frame the graph cannot see (a worker thread, a builtin, a library). There is no call site +// for it, so before this file the method had NO caller here at all, and `impact +// OrderService.cancel` did not name the function that queued it. The engine already KNEW what +// each of these expressions denotes -- expr_denotes_method resolves `self._do_work` through the +// receiver's MRO exactly as it resolves `self._do_work()` -- and dropped the answer because no +// CALL wrapped it. +// +// ── WHAT IT EMITS ─────────────────────────────────────────────────────────── +// call_chain_edge(Expr, Caller, "-", Target, "client", "callback_registered", "METHOD_REFERENCE") +// Like PROPERTY_READ and the protocol rows in call_chain.dl, an edge with no call site: Site is +// the reference expression itself, and the kind is its own so a consumer can always tell it +// from a written call. The tier is the one every other front end uses for a function handed +// over (`xs.forEach(f)`, a JSX handler attribute): HANDED OVER, never `resolved`, so an answer +// says "handed over as a value" instead of "calls it". +// +// ── WHAT IT REFUSES ───────────────────────────────────────────────────────── +// - ONLY WHAT IS RESOLVED. The target comes from the same resolution an ordinary call's +// callee gets: a receiver typed through expr_type and the MRO, a class named by its +// declaration, a name bound by its scope, a module member. `thing.cancel` on an untyped +// parameter denotes nothing, so it emits nothing -- the same blind spot `thing.cancel()` +// has. No rule here matches by name. +// - NOT A FLOWED VALUE. `def run(cb): pool.submit(cb)` denotes, through +// param_arg_method, every function any caller passed in. The reference that matters is +// the one the caller WROTE (`run(self._work)`), which already has its own row; crediting +// `run` as well would make it reach the union of every callable handed to it. The same +// holds for an attribute that holds a function (see (b) below). +// - NOT A CALLEE, NOT A WRITE, NOT AN OBJECT OF ANOTHER ATTRIBUTE. `self.m()` is a call +// (call_chain.dl owns it); `self.m = x` is a store; `self.m.__name__` and +// `self.m.cache_clear()` read the function object and never run it. +// - NOT A PROPERTY, NOT A DECORATOR. `order.total` on a @property (or @cached_property) +// reads the VALUE the getter computes: call_chain.dl already emits that as PROPERTY_READ, +// and the function object is never handed anywhere. `@audited` is a decorator +// application, which call_chain.dl emits as DECORATOR_*. Before these two guards they +// were the two largest wrong classes in a sampled review. +// - NOT THE ARGUMENT OF A ONE-DUNDER BUILTIN. `str(fn)`, `repr(fn)`, `hash(fn)` +// (builtins.dl's builtin_protocol_slot) run a dunder of the FUNCTION OBJECT and never call +// it; a test asserting on `str(svc.cancel)` is not a caller of cancel. Other inspecting +// builtins (`id`, `callable`, `isinstance`) are not catalogued and still count. +// - NOT A COMPARISON OPERAND. `assert job.on_done is svc.cancel` tests identity. +// - ONLY NAMES AND ATTRIBUTE READS. A lambda, a subscript (`table[k]`) and a call that +// returns a function (`partial(...)`, a factory) also denote methods; each already has its +// own edge where it is invoked, or IS a call with its own row. +// The receiver-typing, decorator-chain and import rules this reads all live in resolution/; +// nothing here widens them. +// ============================================================================ + +.decl method_ref_expr(e:symbol, m:symbol) +// (a) a NAME: what resolution/ says it denotes -- a def bound in scope, an import, a +// decorated name's wrapper -- minus a name that holds a flowed VALUE: a parameter, or a +// local or module name assigned from an expression (`cb = job.on_done`). That +// assignment's right-hand side is credited where it is written when it is a reference +// itself; when it is a field read, what the name holds is the union of every write to that +// field, which was the largest wrong shape in a sampled review (`assert cb is not None` +// credited with every callback any job was ever given). +method_ref_expr(e, m) :- + expr_node("client", "NAME_REFERENCE", _, _, e), + expr_denotes_method("client", e, m), + !expr_names_param("client", e, _), + !method_ref_name_holds_value(e). +.decl method_ref_name_holds_value(e:symbol) +method_ref_name_holds_value(e) :- + expr_node("client", "NAME_REFERENCE", _, _, e), + expr_binding("client", b, ctx, e), ctx != "STORE", + binding_lookup("client", b, b2), binding_value_method("client", b2, _). +// (b) an ATTRIBUTE: a method found through the typed receiver's MRO (`svc.cancel`, +// `self._work`), or a module member (`views.index`, decorated or not). NOT an attribute +// that merely HOLDS a function some write put there (`job.on_done`): that is a flowed +// value like a parameter's, and a sampled review found exactly that shape wrong: +// `job.on_done is NO_OP`, credited to whatever any write stored. +method_ref_expr(e, m) :- expr_denotes_bound_method("client", e, m). +method_ref_expr(e, m) :- + expr_node("client", "ATTRIBUTE_ACCESS", _, _, e), + expr_attr_names_method("client", e, _), + expr_denotes_method("client", e, m). +// (c) an UNBOUND method read off its class: `map(OrderService.convert, rows)`. The class +// object types the receiver, and the MRO names the method -- the rule +// callee-resolution.dl uses for `OrderService.convert(svc, row)`, without the call. +method_ref_expr(e, m) :- + expr_node("client", "ATTRIBUTE_ACCESS", _, n, e), + expr_parent("client", e, "ATTRIBUTE_OBJECT", _, obj), + expr_type_class_object("client", obj, t), + mro_lookup("client", t, n, m). + +// the shape guards: read as a value, not called, not a write, not the object of another read +.decl method_ref_site(e:symbol, m:symbol) +method_ref_site(e, m) :- + method_ref_expr(e, m), + !call_callee_expr(_, e), + !expr_name_context("client", "STORE", e), !expr_name_context("client", "DEL", e), + !expr_parent("client", _, "ATTRIBUTE_OBJECT", _, e), + !decorator_expr("client", e, _), + !builtin_protocol_arg(_, _, e), + !method_ref_compared(e), + method_kind("client", kind, _, m), + kind != "MODULE_INITIALIZER", kind != "CLASS_INITIALIZER", kind != "OVERLOAD_STUB", kind != "LAMBDA", + kind != "PROPERTY_GETTER", kind != "PROPERTY_SETTER", kind != "PROPERTY_DELETER". + +// an operand of a comparison: `job.on_done is svc.cancel`, `h in self.handlers`, `f == g` test +// the function object's identity or equality and never call it +.decl method_ref_compared(e:symbol) +method_ref_compared(e) :- method_ref_expr(e, _), expr_parent("client", c, _, _, e), expr_node("client", "COMPARISON", _, _, c). + +// the caller: the function or lambda whose body evaluates the reference. A module-level +// reference and a parameter default are owned by the synthetic , a class-body one by +// -- the parser records that owner on the expression, as it does for a call. +.decl method_ref_caller(e:symbol, c:symbol) +method_ref_caller(e, c) :- method_ref_site(e, _), expr_owner("client", "METHOD", c, _, e). +method_ref_caller(e, c) :- method_ref_site(e, _), expr_owner("client", "LAMBDA", c, _, e). +method_ref_caller(e, c) :- method_ref_site(e, _), + !expr_owner("client", "METHOD", _, _, e), !expr_owner("client", "LAMBDA", _, _, e), + expr_ultimate_method("client", e, c). + +call_chain_edge(e, c, "-", m, "client", "callback_registered", "METHOD_REFERENCE") :- + method_ref_site(e, m), method_ref_caller(e, c). diff --git a/graph/test/python/expected/01-plain-calls.edges b/graph/test/python/expected/01-plain-calls.edges index c37ab9bcd..9df3ca724 100644 --- a/graph/test/python/expected/01-plain-calls.edges +++ b/graph/test/python/expected/01-plain-calls.edges @@ -1,4 +1,5 @@ boundary_lib SIMPLE_CALL main.main -> builtin:print +callback_registered METHOD_REFERENCE main.make_adder -> main.make_adder..add known_edge SIMPLE_CALL main. -> main.main known_edge SIMPLE_CALL main.main -> main.make_adder known_edge SIMPLE_CALL main.main -> main.make_adder..add diff --git a/graph/test/python/expected/01-plain-calls.tiers b/graph/test/python/expected/01-plain-calls.tiers index 5b235883c..0c7ce9f47 100644 --- a/graph/test/python/expected/01-plain-calls.tiers +++ b/graph/test/python/expected/01-plain-calls.tiers @@ -1,10 +1,12 @@ -distinct call sites emitted: 12 +distinct call sites emitted: 13 --- by tier: edge ROWS, and the distinct SITES they cover --- 3 rows 3 sites boundary_lib + 1 rows 1 sites callback_registered 9 rows 9 sites known_edge --- edge rows by call kind --- + 1 METHOD_REFERENCE 12 SIMPLE_CALL --- unresolved reasons --- @@ -16,7 +18,7 @@ distinct call sites emitted: 12 9 known_edge --- reconciling rows against the conserved site count --- - edge rows 12 + edge rows 13 minus extra rows from multi-target sites 0 - = tier/site pairs 12 + = tier/site pairs 13 engine's conserved site total 12 diff --git a/graph/test/python/expected/07-decorators.edges b/graph/test/python/expected/07-decorators.edges index 41e8ab4f3..fa1975f0d 100644 --- a/graph/test/python/expected/07-decorators.edges +++ b/graph/test/python/expected/07-decorators.edges @@ -8,6 +8,11 @@ boundary_lib SIMPLE_CALL main.inner_deco..inner_w -> builtin:str boundary_lib SIMPLE_CALL main.main -> builtin:print boundary_lib SIMPLE_CALL main.outer_deco..outer_w -> builtin:str boundary_lib SIMPLE_CALL main.wrapping..wrapper -> builtin:str +callback_registered METHOD_REFERENCE main.inner_deco -> main.inner_deco..inner_w +callback_registered METHOD_REFERENCE main.outer_deco -> main.outer_deco..outer_w +callback_registered METHOD_REFERENCE main.replacing -> main.replacing..substitute +callback_registered METHOD_REFERENCE main.route -> main.route..register +callback_registered METHOD_REFERENCE main.wrapping -> main.wrapping..wrapper known_edge DECORATOR_APPLICATION main. -> main.route..register known_edge DECORATOR_BARE main. -> main.inner_deco known_edge DECORATOR_BARE main. -> main.outer_deco diff --git a/graph/test/python/expected/07-decorators.tiers b/graph/test/python/expected/07-decorators.tiers index e9686fea6..33c95f323 100644 --- a/graph/test/python/expected/07-decorators.tiers +++ b/graph/test/python/expected/07-decorators.tiers @@ -1,14 +1,16 @@ -distinct call sites emitted: 30 +distinct call sites emitted: 35 --- by tier: edge ROWS, and the distinct SITES they cover --- 3 rows 3 sites ambiguous_unknown 11 rows 11 sites boundary_lib + 5 rows 5 sites callback_registered 16 rows 16 sites known_edge --- edge rows by call kind --- 4 DECORATOR_APPLICATION 5 DECORATOR_BARE 4 DECORATOR_CALL + 5 METHOD_REFERENCE 16 SIMPLE_CALL 1 SUBSCRIPT_CALL @@ -22,7 +24,7 @@ distinct call sites emitted: 30 16 known_edge --- reconciling rows against the conserved site count --- - edge rows 30 + edge rows 35 minus extra rows from multi-target sites 0 - = tier/site pairs 30 + = tier/site pairs 35 engine's conserved site total 30 diff --git a/graph/test/python/expected/08-callables.edges b/graph/test/python/expected/08-callables.edges index 5b0c619d2..24c61eb13 100644 --- a/graph/test/python/expected/08-callables.edges +++ b/graph/test/python/expected/08-callables.edges @@ -2,6 +2,8 @@ ambiguous_unknown SELF_CALL main.Holder.use_partial -> - boundary_lib METHOD_CALL main.Holder.__init__ -> external:functools.partial boundary_lib METHOD_CALL main.main -> external:functools.partial boundary_lib SIMPLE_CALL main.main -> builtin:print +callback_registered METHOD_REFERENCE main.Holder.__init__ -> main.add +callback_registered METHOD_REFERENCE main.main -> main.add known_edge CHAINED_CALL main.main -> main.Holder.use known_edge CHAINED_CALL main.main -> main.Holder.use_partial known_edge SELF_CALL main.Holder.use -> main.add diff --git a/graph/test/python/expected/08-callables.tiers b/graph/test/python/expected/08-callables.tiers index 9d83aad23..7a2631ca0 100644 --- a/graph/test/python/expected/08-callables.tiers +++ b/graph/test/python/expected/08-callables.tiers @@ -1,13 +1,15 @@ -distinct call sites emitted: 20 +distinct call sites emitted: 23 --- by tier: edge ROWS, and the distinct SITES they cover --- 1 rows 1 sites ambiguous_unknown 8 rows 8 sites boundary_lib + 3 rows 3 sites callback_registered 11 rows 11 sites known_edge --- edge rows by call kind --- 2 CHAINED_CALL 3 METHOD_CALL + 3 METHOD_REFERENCE 3 SELF_CALL 11 SIMPLE_CALL 1 SUBSCRIPT_CALL @@ -22,7 +24,7 @@ distinct call sites emitted: 20 11 known_edge --- reconciling rows against the conserved site count --- - edge rows 20 + edge rows 23 minus extra rows from multi-target sites 0 - = tier/site pairs 20 + = tier/site pairs 23 engine's conserved site total 20 diff --git a/graph/test/python/expected/09-args-forwarding.edges b/graph/test/python/expected/09-args-forwarding.edges index d05a79897..1dfc354c7 100644 --- a/graph/test/python/expected/09-args-forwarding.edges +++ b/graph/test/python/expected/09-args-forwarding.edges @@ -1,5 +1,6 @@ boundary_lib SIMPLE_CALL main.main -> builtin:print boundary_lib SIMPLE_CALL main.target -> builtin:len +callback_registered METHOD_REFERENCE main.main -> main.target known_edge DYNAMIC_CALL main.main -> main.Delegator.__call__ known_edge SELF_CALL main.Delegator.__call__ -> main.target known_edge SIMPLE_CALL main. -> main.main diff --git a/graph/test/python/expected/09-args-forwarding.tiers b/graph/test/python/expected/09-args-forwarding.tiers index 61fade854..13ce90bc8 100644 --- a/graph/test/python/expected/09-args-forwarding.tiers +++ b/graph/test/python/expected/09-args-forwarding.tiers @@ -1,11 +1,13 @@ -distinct call sites emitted: 15 +distinct call sites emitted: 16 --- by tier: edge ROWS, and the distinct SITES they cover --- 5 rows 5 sites boundary_lib + 1 rows 1 sites callback_registered 10 rows 10 sites known_edge --- edge rows by call kind --- 1 DYNAMIC_CALL + 1 METHOD_REFERENCE 1 SELF_CALL 13 SIMPLE_CALL @@ -18,7 +20,7 @@ distinct call sites emitted: 15 10 known_edge --- reconciling rows against the conserved site count --- - edge rows 15 + edge rows 16 minus extra rows from multi-target sites 0 - = tier/site pairs 15 + = tier/site pairs 16 engine's conserved site total 15 diff --git a/graph/test/python/expected/12-blind-spots.edges b/graph/test/python/expected/12-blind-spots.edges index 014a110bf..910099c29 100644 --- a/graph/test/python/expected/12-blind-spots.edges +++ b/graph/test/python/expected/12-blind-spots.edges @@ -7,6 +7,7 @@ boundary_lib SIMPLE_CALL main.Installer.__new__ -> builtin:super boundary_lib SIMPLE_CALL main.call_by_computed_name -> builtin:getattr boundary_lib SIMPLE_CALL main.main -> builtin:print boundary_lib SUPER_CALL main.Installer.__new__ -> builtin:type.__new__ +callback_registered METHOD_REFERENCE main. -> main.patched known_edge METACLASS_CREATION main. -> main.Installer.__new__ known_edge METHOD_CALL main.main -> main.patched known_edge SIMPLE_CALL main. -> main.main diff --git a/graph/test/python/expected/12-blind-spots.tiers b/graph/test/python/expected/12-blind-spots.tiers index c2de53b23..77b9517f7 100644 --- a/graph/test/python/expected/12-blind-spots.tiers +++ b/graph/test/python/expected/12-blind-spots.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 19 +distinct call sites emitted: 20 --- by tier: edge ROWS, and the distinct SITES they cover --- 5 rows 5 sites ambiguous_unknown 10 rows 10 sites boundary_lib + 1 rows 1 sites callback_registered 4 rows 4 sites known_edge --- edge rows by call kind --- @@ -10,6 +11,7 @@ distinct call sites emitted: 19 1 DYNAMIC_CALL 1 METACLASS_CREATION 2 METHOD_CALL + 1 METHOD_REFERENCE 13 SIMPLE_CALL 1 SUPER_CALL @@ -25,8 +27,8 @@ distinct call sites emitted: 19 3 known_edge --- reconciling rows against the conserved site count --- - edge rows 19 + edge rows 20 minus extra rows from multi-target sites 0 - = tier/site pairs 19 + = tier/site pairs 20 engine's conserved site total 18 of which 1 are METACLASS_CREATION, an edge with no call site (README decision 5) diff --git a/graph/test/python/expected/17-rebound-metaclass-slot.edges b/graph/test/python/expected/17-rebound-metaclass-slot.edges index 8555ff828..01bd9313b 100644 --- a/graph/test/python/expected/17-rebound-metaclass-slot.edges +++ b/graph/test/python/expected/17-rebound-metaclass-slot.edges @@ -10,6 +10,7 @@ boundary_lib SUPER_CALL main.EarlyMeta.__init__ -> builtin:type.__init__ boundary_lib SUPER_CALL main.EarlyMeta.__new__ -> builtin:type.__new__ boundary_lib SUPER_CALL main.LateMeta.__init__ -> builtin:type.__init__ boundary_lib SUPER_CALL main.LateMeta.__new__ -> builtin:type.__new__ +callback_registered METHOD_REFERENCE main. -> main._late_new known_edge CHAINED_CALL main.main -> main.After.run known_edge CHAINED_CALL main.main -> main.AlsoBefore.run known_edge CHAINED_CALL main.main -> main.Before.run diff --git a/graph/test/python/expected/17-rebound-metaclass-slot.tiers b/graph/test/python/expected/17-rebound-metaclass-slot.tiers index 36fff263c..7b9785d53 100644 --- a/graph/test/python/expected/17-rebound-metaclass-slot.tiers +++ b/graph/test/python/expected/17-rebound-metaclass-slot.tiers @@ -1,14 +1,16 @@ -distinct call sites emitted: 20 +distinct call sites emitted: 22 --- by tier: edge ROWS, and the distinct SITES they cover --- 3 rows 3 sites ambiguous_unknown 11 rows 11 sites boundary_lib + 2 rows 2 sites callback_registered 8 rows 6 sites known_edge --- edge rows by call kind --- 3 CHAINED_CALL 5 METACLASS_CREATION 1 METHOD_CALL + 2 METHOD_REFERENCE 9 SIMPLE_CALL 4 SUPER_CALL @@ -23,8 +25,8 @@ distinct call sites emitted: 20 3 known_edge --- reconciling rows against the conserved site count --- - edge rows 22 + edge rows 24 minus extra rows from multi-target sites 2 - = tier/site pairs 20 + = tier/site pairs 22 engine's conserved site total 17 of which 5 are METACLASS_CREATION, an edge with no call site (README decision 5) diff --git a/graph/test/python/expected/21-url-and-signal-dispatch.edges b/graph/test/python/expected/21-url-and-signal-dispatch.edges index 9d07c714d..0d85ec445 100644 --- a/graph/test/python/expected/21-url-and-signal-dispatch.edges +++ b/graph/test/python/expected/21-url-and-signal-dispatch.edges @@ -3,6 +3,13 @@ ambiguous_unknown DECORATOR_APPLICATION main. -> - ambiguous_unknown METHOD_CALL main.not_a_signal_send -> - boundary_lib SIMPLE_CALL main. -> builtin:object.__init__ boundary_lib SIMPLE_CALL signals. -> builtin:object.__init__ +callback_registered METHOD_REFERENCE main. -> main.detail +callback_registered METHOD_REFERENCE main. -> main.index +callback_registered METHOD_REFERENCE main. -> main.legacy +callback_registered METHOD_REFERENCE main. -> main.on_order_connected +callback_registered METHOD_REFERENCE main.not_a_route_table -> main.detail +callback_registered METHOD_REFERENCE urls. -> views.profile +callback_registered METHOD_REFERENCE urls. -> views.settings_page known_edge DECORATOR_CALL handlers. -> handlers.receiver known_edge DECORATOR_CALL main. -> main.receiver known_edge METHOD_CALL main. -> main.Signal.connect diff --git a/graph/test/python/expected/21-url-and-signal-dispatch.tiers b/graph/test/python/expected/21-url-and-signal-dispatch.tiers index 56e700521..03db6da82 100644 --- a/graph/test/python/expected/21-url-and-signal-dispatch.tiers +++ b/graph/test/python/expected/21-url-and-signal-dispatch.tiers @@ -1,14 +1,16 @@ -distinct call sites emitted: 32 +distinct call sites emitted: 39 --- by tier: edge ROWS, and the distinct SITES they cover --- 5 rows 5 sites ambiguous_unknown 4 rows 4 sites boundary_lib + 7 rows 7 sites callback_registered 23 rows 23 sites known_edge --- edge rows by call kind --- 4 DECORATOR_APPLICATION 4 DECORATOR_CALL 4 METHOD_CALL + 7 METHOD_REFERENCE 20 SIMPLE_CALL --- unresolved reasons --- @@ -22,7 +24,7 @@ distinct call sites emitted: 32 23 known_edge --- reconciling rows against the conserved site count --- - edge rows 32 + edge rows 39 minus extra rows from multi-target sites 0 - = tier/site pairs 32 + = tier/site pairs 39 engine's conserved site total 32 diff --git a/graph/test/python/expected/22-di-providers.edges b/graph/test/python/expected/22-di-providers.edges index f73dff093..f02424a43 100644 --- a/graph/test/python/expected/22-di-providers.edges +++ b/graph/test/python/expected/22-di-providers.edges @@ -3,6 +3,12 @@ ambiguous_unknown DECORATOR_APPLICATION routes. -> - ambiguous_unknown METHOD_CALL main.read_item -> - boundary_lib SIMPLE_CALL main. -> builtin:object.__init__ boundary_lib SIMPLE_CALL routes. -> builtin:object.__init__ +callback_registered METHOD_REFERENCE deps. -> deps.get_repo +callback_registered METHOD_REFERENCE main. -> main.get_service +callback_registered METHOD_REFERENCE main. -> main.get_settings +callback_registered METHOD_REFERENCE main. -> main.get_user +callback_registered METHOD_REFERENCE routes. -> deps.get_page_size +callback_registered METHOD_REFERENCE routes. -> deps.get_repo known_edge DECORATOR_CALL main. -> main._App.get known_edge DECORATOR_CALL main. -> main._App.post known_edge DECORATOR_CALL routes. -> routes._Router.get diff --git a/graph/test/python/expected/22-di-providers.tiers b/graph/test/python/expected/22-di-providers.tiers index 193e49ac0..c1e8f4c11 100644 --- a/graph/test/python/expected/22-di-providers.tiers +++ b/graph/test/python/expected/22-di-providers.tiers @@ -1,14 +1,16 @@ -distinct call sites emitted: 22 +distinct call sites emitted: 29 --- by tier: edge ROWS, and the distinct SITES they cover --- 4 rows 4 sites ambiguous_unknown 2 rows 2 sites boundary_lib + 7 rows 7 sites callback_registered 16 rows 16 sites known_edge --- edge rows by call kind --- 3 DECORATOR_APPLICATION 3 DECORATOR_CALL 1 METHOD_CALL + 7 METHOD_REFERENCE 15 SIMPLE_CALL --- unresolved reasons --- @@ -22,7 +24,7 @@ distinct call sites emitted: 22 16 known_edge --- reconciling rows against the conserved site count --- - edge rows 22 + edge rows 29 minus extra rows from multi-target sites 0 - = tier/site pairs 22 + = tier/site pairs 29 engine's conserved site total 22 diff --git a/graph/test/python/expected/25-imported-decorated-names.edges b/graph/test/python/expected/25-imported-decorated-names.edges index 7f267280d..1c4482423 100644 --- a/graph/test/python/expected/25-imported-decorated-names.edges +++ b/graph/test/python/expected/25-imported-decorated-names.edges @@ -3,6 +3,8 @@ ambiguous_unknown DECORATOR_ATTRIBUTE pkg.lib. -> - ambiguous_unknown METHOD_CALL main.run -> - ambiguous_unknown SIMPLE_CALL main.run -> - boundary_lib DECORATOR_CALL pkg.lib.wraps_deco -> external:functools.wraps +callback_registered METHOD_REFERENCE pkg.lib.plain_deco -> pkg.lib.plain_deco..wrapper +callback_registered METHOD_REFERENCE pkg.lib.wraps_deco -> pkg.lib.wraps_deco..wrapper known_edge DECORATOR_BARE pkg.lib. -> pkg.lib.passthrough known_edge DECORATOR_BARE pkg.lib. -> pkg.lib.plain_deco known_edge DECORATOR_BARE pkg.lib. -> pkg.lib.wraps_deco diff --git a/graph/test/python/expected/25-imported-decorated-names.tiers b/graph/test/python/expected/25-imported-decorated-names.tiers index b4fa51e62..2cd48680e 100644 --- a/graph/test/python/expected/25-imported-decorated-names.tiers +++ b/graph/test/python/expected/25-imported-decorated-names.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 24 +distinct call sites emitted: 26 --- by tier: edge ROWS, and the distinct SITES they cover --- 4 rows 4 sites ambiguous_unknown 1 rows 1 sites boundary_lib + 2 rows 2 sites callback_registered 19 rows 19 sites known_edge --- edge rows by call kind --- @@ -11,6 +12,7 @@ distinct call sites emitted: 24 3 DECORATOR_BARE 1 DECORATOR_CALL 5 METHOD_CALL + 2 METHOD_REFERENCE 13 SIMPLE_CALL --- unresolved reasons --- @@ -26,7 +28,7 @@ distinct call sites emitted: 24 19 known_edge --- reconciling rows against the conserved site count --- - edge rows 24 + edge rows 26 minus extra rows from multi-target sites 0 - = tier/site pairs 24 + = tier/site pairs 26 engine's conserved site total 24 diff --git a/graph/test/python/expected/27-returned-expression.edges b/graph/test/python/expected/27-returned-expression.edges index fed3df32d..c8699cfe3 100644 --- a/graph/test/python/expected/27-returned-expression.edges +++ b/graph/test/python/expected/27-returned-expression.edges @@ -6,6 +6,9 @@ boundary_lib METHOD_CALL main.Expr.copy -> external:copy.copy boundary_lib SIMPLE_CALL main. -> builtin:object.__init__ boundary_lib SIMPLE_CALL main.Expr.__init__ -> builtin:object.__init__ boundary_lib SIMPLE_CALL main.Expr.label -> builtin:str +callback_registered METHOD_REFERENCE main.Library.filter -> main.Library.filter..dec +callback_registered METHOD_REFERENCE main.defer_result -> main.defer_result..decorator +callback_registered METHOD_REFERENCE main.defer_result..decorator -> main.defer_result..decorator..wrapper known_edge DECORATOR_APPLICATION main. -> main.Library.filter..dec known_edge DECORATOR_BARE main. -> main.defer_text known_edge DECORATOR_CALL main. -> main.Library.filter diff --git a/graph/test/python/expected/27-returned-expression.tiers b/graph/test/python/expected/27-returned-expression.tiers index f44e09e11..61c44026e 100644 --- a/graph/test/python/expected/27-returned-expression.tiers +++ b/graph/test/python/expected/27-returned-expression.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 23 +distinct call sites emitted: 26 --- by tier: edge ROWS, and the distinct SITES they cover --- 2 rows 2 sites ambiguous_unknown 6 rows 6 sites boundary_lib + 3 rows 3 sites callback_registered 15 rows 15 sites known_edge 2 rows 1 sites multi_inferred @@ -12,6 +13,7 @@ distinct call sites emitted: 23 3 DECORATOR_CALL 1 DYNAMIC_CALL 5 METHOD_CALL + 3 METHOD_REFERENCE 3 SELF_CALL 10 SIMPLE_CALL @@ -27,7 +29,7 @@ distinct call sites emitted: 23 1 multi_inferred --- reconciling rows against the conserved site count --- - edge rows 25 + edge rows 28 minus extra rows from multi-target sites 1 - = tier/site pairs 24 + = tier/site pairs 27 engine's conserved site total 23 diff --git a/graph/test/python/expected/28-assigned-members.edges b/graph/test/python/expected/28-assigned-members.edges index c1af28b71..355e252d5 100644 --- a/graph/test/python/expected/28-assigned-members.edges +++ b/graph/test/python/expected/28-assigned-members.edges @@ -1,6 +1,8 @@ boundary_lib SIMPLE_CALL main. -> external:TypeVar boundary_lib SIMPLE_CALL main.Schema.__init__ -> builtin:object.__init__ boundary_lib SIMPLE_CALL main.main -> builtin:print +callback_registered METHOD_REFERENCE main.init -> main.Internals.parse +callback_registered METHOD_REFERENCE main.init -> main.init.._run known_edge SIMPLE_CALL main. -> main.main known_edge SIMPLE_CALL main.main -> main.Schema.__init__ known_edge SIMPLE_CALL main.main -> main.init diff --git a/graph/test/python/expected/28-assigned-members.tiers b/graph/test/python/expected/28-assigned-members.tiers index 4f1ba55ef..c4536e576 100644 --- a/graph/test/python/expected/28-assigned-members.tiers +++ b/graph/test/python/expected/28-assigned-members.tiers @@ -1,12 +1,14 @@ -distinct call sites emitted: 19 +distinct call sites emitted: 21 --- by tier: edge ROWS, and the distinct SITES they cover --- 4 rows 4 sites boundary_lib + 2 rows 2 sites callback_registered 7 rows 7 sites known_edge 30 rows 8 sites multi_inferred --- edge rows by call kind --- 30 METHOD_CALL + 2 METHOD_REFERENCE 11 SIMPLE_CALL --- unresolved reasons --- @@ -19,7 +21,7 @@ distinct call sites emitted: 19 8 multi_inferred --- reconciling rows against the conserved site count --- - edge rows 41 + edge rows 43 minus extra rows from multi-target sites 22 - = tier/site pairs 19 + = tier/site pairs 21 engine's conserved site total 19 diff --git a/graph/test/python/expected/33-url-view-forms.edges b/graph/test/python/expected/33-url-view-forms.edges index c05006003..0de9e0620 100644 --- a/graph/test/python/expected/33-url-view-forms.edges +++ b/graph/test/python/expected/33-url-view-forms.edges @@ -6,6 +6,16 @@ boundary_lib SIMPLE_CALL catalog.urls. -> external:path boundary_lib SIMPLE_CALL shop.urls. -> external:include boundary_lib SIMPLE_CALL shop.urls. -> external:path boundary_lib SIMPLE_CALL shop.urls. -> external:re_path +callback_registered METHOD_REFERENCE catalog.urls. -> catalog.views.catalog_index +callback_registered METHOD_REFERENCE shop.decorators.audited -> shop.decorators.audited..wrapper +callback_registered METHOD_REFERENCE shop.urls. -> shop.decorators.audited..wrapper +callback_registered METHOD_REFERENCE shop.urls. -> shop.views.api_status +callback_registered METHOD_REFERENCE shop.urls. -> shop.views.order_credit_note +callback_registered METHOD_REFERENCE shop.urls. -> shop.views.order_draft +callback_registered METHOD_REFERENCE shop.urls. -> shop.views.order_export +callback_registered METHOD_REFERENCE shop.urls. -> shop.views.order_legacy +callback_registered METHOD_REFERENCE shop.urls. -> shop.views.order_list +callback_registered METHOD_REFERENCE shop.urls. -> shop.views.order_report known_edge DECORATOR_BARE shop.views. -> shop.decorators.audited known_edge SIMPLE_CALL shop.decorators.audited..wrapper -> shop.views.order_history known_edge SIMPLE_CALL shop.views.BaseWidgetView.get -> shop.views._render diff --git a/graph/test/python/expected/33-url-view-forms.tiers b/graph/test/python/expected/33-url-view-forms.tiers index f05de8d3f..fd0afb6f1 100644 --- a/graph/test/python/expected/33-url-view-forms.tiers +++ b/graph/test/python/expected/33-url-view-forms.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 51 +distinct call sites emitted: 61 --- by tier: edge ROWS, and the distinct SITES they cover --- 6 rows 6 sites ambiguous_unknown 27 rows 27 sites boundary_lib + 10 rows 10 sites callback_registered 18 rows 18 sites known_edge --- edge rows by call kind --- @@ -10,6 +11,7 @@ distinct call sites emitted: 51 5 DECORATOR_BARE 1 DECORATOR_CALL 1 METHOD_CALL + 10 METHOD_REFERENCE 43 SIMPLE_CALL --- unresolved reasons --- @@ -24,7 +26,7 @@ distinct call sites emitted: 51 18 known_edge --- reconciling rows against the conserved site count --- - edge rows 51 + edge rows 61 minus extra rows from multi-target sites 0 - = tier/site pairs 51 + = tier/site pairs 61 engine's conserved site total 51 diff --git a/graph/test/python/expected/36-call-through-model-decorators.edges b/graph/test/python/expected/36-call-through-model-decorators.edges index 266e2812c..69c96d257 100644 --- a/graph/test/python/expected/36-call-through-model-decorators.edges +++ b/graph/test/python/expected/36-call-through-model-decorators.edges @@ -6,6 +6,7 @@ boundary_lib DECORATOR_CALL app.orders. -> external:pydantic.validate_ca boundary_lib METHOD_CALL app.checkout.show -> builtin:float.hex boundary_lib SIMPLE_CALL app.orders.apply_discount -> builtin:round boundary_lib SIMPLE_CALL app.orders.apply_tax -> builtin:round +callback_registered METHOD_REFERENCE app.orders.audit -> app.orders.audit..wrapper known_edge DECORATOR_BARE app.orders. -> app.orders.audit known_edge PROPERTY_READ app.checkout.show -> app.orders.Order.grand_total known_edge PROPERTY_READ app.checkout.show -> app.orders.Order.net_total diff --git a/graph/test/python/expected/36-call-through-model-decorators.tiers b/graph/test/python/expected/36-call-through-model-decorators.tiers index 2e14b72cd..e9d9a11ce 100644 --- a/graph/test/python/expected/36-call-through-model-decorators.tiers +++ b/graph/test/python/expected/36-call-through-model-decorators.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 19 +distinct call sites emitted: 20 --- by tier: edge ROWS, and the distinct SITES they cover --- 4 rows 4 sites ambiguous_unknown 6 rows 6 sites boundary_lib + 1 rows 1 sites callback_registered 9 rows 9 sites known_edge --- edge rows by call kind --- @@ -10,6 +11,7 @@ distinct call sites emitted: 19 6 DECORATOR_BARE 1 DECORATOR_CALL 1 METHOD_CALL + 1 METHOD_REFERENCE 3 PROPERTY_READ 7 SIMPLE_CALL @@ -24,8 +26,8 @@ distinct call sites emitted: 19 6 known_edge --- reconciling rows against the conserved site count --- - edge rows 19 + edge rows 20 minus extra rows from multi-target sites 0 - = tier/site pairs 19 + = tier/site pairs 20 engine's conserved site total 16 of which 3 are PROPERTY_READ, an edge with no call site (README decision 5) diff --git a/graph/test/python/expected/37-model-signals.edges b/graph/test/python/expected/37-model-signals.edges index 675318292..aed5cf7af 100644 --- a/graph/test/python/expected/37-model-signals.edges +++ b/graph/test/python/expected/37-model-signals.edges @@ -9,4 +9,5 @@ boundary_lib DECORATOR_CALL models. -> external:receiver boundary_lib METHOD_CALL models. -> external:post_save.connect boundary_lib METHOD_CALL models.Invoice. -> external:models.IntegerField boundary_lib METHOD_CALL models.Order. -> external:models.CharField +callback_registered METHOD_REFERENCE models. -> models.audit_order known_edge METHOD_CALL views.save_cache -> views.Cache.save diff --git a/graph/test/python/expected/37-model-signals.tiers b/graph/test/python/expected/37-model-signals.tiers index e87fd3fad..10a3c4b9a 100644 --- a/graph/test/python/expected/37-model-signals.tiers +++ b/graph/test/python/expected/37-model-signals.tiers @@ -1,14 +1,16 @@ -distinct call sites emitted: 14 +distinct call sites emitted: 15 --- by tier: edge ROWS, and the distinct SITES they cover --- 8 rows 8 sites ambiguous_unknown 5 rows 5 sites boundary_lib + 1 rows 1 sites callback_registered 1 rows 1 sites known_edge --- edge rows by call kind --- 2 DECORATOR_APPLICATION 2 DECORATOR_CALL 10 METHOD_CALL + 1 METHOD_REFERENCE --- unresolved reasons --- 2 decorator_factory_result_untyped @@ -23,7 +25,7 @@ distinct call sites emitted: 14 1 known_edge --- reconciling rows against the conserved site count --- - edge rows 14 + edge rows 15 minus extra rows from multi-target sites 0 - = tier/site pairs 14 + = tier/site pairs 15 engine's conserved site total 14 diff --git a/graph/test/python/expected/38-app-wiring.edges b/graph/test/python/expected/38-app-wiring.edges index 39d93d28d..8813d217c 100644 --- a/graph/test/python/expected/38-app-wiring.edges +++ b/graph/test/python/expected/38-app-wiring.edges @@ -17,6 +17,12 @@ boundary_lib SIMPLE_CALL main. -> external:Starlette boundary_lib SIMPLE_CALL main. -> external:WebSocketRoute boundary_lib SIMPLE_CALL shop.jobs.nightly -> external:call_command boundary_lib SIMPLE_CALL shop.jobs.unknown -> external:call_command +callback_registered METHOD_REFERENCE main. -> main.cancel +callback_registered METHOD_REFERENCE main. -> main.feed +callback_registered METHOD_REFERENCE main. -> main.health +callback_registered METHOD_REFERENCE main. -> main.lifespan +callback_registered METHOD_REFERENCE main. -> main.plugin_hook +callback_registered METHOD_REFERENCE main. -> main.tick known_edge METHOD_CALL main. -> main.Registry.add_route known_edge SIMPLE_CALL main. -> main.configure known_edge SIMPLE_CALL shop.management.commands.close_orders.Command.handle -> shop.orders.close_all diff --git a/graph/test/python/expected/38-app-wiring.tiers b/graph/test/python/expected/38-app-wiring.tiers index 8496a4b97..ebe6e2258 100644 --- a/graph/test/python/expected/38-app-wiring.tiers +++ b/graph/test/python/expected/38-app-wiring.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 26 +distinct call sites emitted: 32 --- by tier: edge ROWS, and the distinct SITES they cover --- 8 rows 8 sites ambiguous_unknown 15 rows 15 sites boundary_lib + 6 rows 6 sites callback_registered 3 rows 3 sites known_edge --- edge rows by call kind --- @@ -11,6 +12,7 @@ distinct call sites emitted: 26 1 DECORATOR_BARE 4 DECORATOR_CALL 3 METHOD_CALL + 6 METHOD_REFERENCE 13 SIMPLE_CALL --- unresolved reasons --- @@ -25,7 +27,7 @@ distinct call sites emitted: 26 3 known_edge --- reconciling rows against the conserved site count --- - edge rows 26 + edge rows 32 minus extra rows from multi-target sites 0 - = tier/site pairs 26 + = tier/site pairs 32 engine's conserved site total 26 diff --git a/graph/test/python/expected/39-model-construction-hooks.edges b/graph/test/python/expected/39-model-construction-hooks.edges index 3c8b20b60..705a286e3 100644 --- a/graph/test/python/expected/39-model-construction-hooks.edges +++ b/graph/test/python/expected/39-model-construction-hooks.edges @@ -14,5 +14,7 @@ boundary_lib SIMPLE_CALL models.Widget.check_size -> builtin:max boundary_lib SIMPLE_CALL models.Widget.dump_size -> builtin:str boundary_lib SIMPLE_CALL service.make_gizmo -> builtin:object.__init__ boundary_lib SIMPLE_CALL service.make_plain -> builtin:object.__init__ +callback_registered METHOD_REFERENCE models.Gizmo. -> models.new_tags +callback_registered METHOD_REFERENCE models.Widget. -> models.new_id known_edge SELF_CALL models.Gadget.__init__ -> models.Gadget.check_size known_edge SIMPLE_CALL service.make_gadget -> models.Gadget.__init__ diff --git a/graph/test/python/expected/39-model-construction-hooks.tiers b/graph/test/python/expected/39-model-construction-hooks.tiers index a4e4b8aff..d1f820899 100644 --- a/graph/test/python/expected/39-model-construction-hooks.tiers +++ b/graph/test/python/expected/39-model-construction-hooks.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 21 +distinct call sites emitted: 23 --- by tier: edge ROWS, and the distinct SITES they cover --- 8 rows 8 sites ambiguous_unknown 11 rows 11 sites boundary_lib + 2 rows 2 sites callback_registered 2 rows 2 sites known_edge --- edge rows by call kind --- @@ -10,6 +11,7 @@ distinct call sites emitted: 21 3 DECORATOR_BARE 3 DECORATOR_CALL 2 METHOD_CALL + 2 METHOD_REFERENCE 1 SELF_CALL 9 SIMPLE_CALL @@ -27,7 +29,7 @@ distinct call sites emitted: 21 2 known_edge --- reconciling rows against the conserved site count --- - edge rows 21 + edge rows 23 minus extra rows from multi-target sites 0 - = tier/site pairs 21 + = tier/site pairs 23 engine's conserved site total 21 diff --git a/graph/test/python/expected/41-di-marker-forms.edges b/graph/test/python/expected/41-di-marker-forms.edges index 0a449dee2..3cc836455 100644 --- a/graph/test/python/expected/41-di-marker-forms.edges +++ b/graph/test/python/expected/41-di-marker-forms.edges @@ -8,3 +8,11 @@ boundary_lib SIMPLE_CALL handlers. -> external:APIRouter boundary_lib SIMPLE_CALL handlers. -> external:Depends boundary_lib SIMPLE_CALL handlers. -> external:FastAPI boundary_lib SIMPLE_CALL other. -> external:Depends +callback_registered METHOD_REFERENCE deps. -> deps.load_cart +callback_registered METHOD_REFERENCE deps.ShapeFilter. -> deps.load_shape_size +callback_registered METHOD_REFERENCE handlers. -> deps.check_app +callback_registered METHOD_REFERENCE handlers. -> deps.check_quota +callback_registered METHOD_REFERENCE handlers. -> deps.check_region +callback_registered METHOD_REFERENCE handlers. -> deps.load_owner +callback_registered METHOD_REFERENCE handlers. -> deps.load_user +callback_registered METHOD_REFERENCE other. -> other.check_other diff --git a/graph/test/python/expected/41-di-marker-forms.tiers b/graph/test/python/expected/41-di-marker-forms.tiers index 85ad730a4..e2ec4f880 100644 --- a/graph/test/python/expected/41-di-marker-forms.tiers +++ b/graph/test/python/expected/41-di-marker-forms.tiers @@ -1,12 +1,14 @@ -distinct call sites emitted: 32 +distinct call sites emitted: 40 --- by tier: edge ROWS, and the distinct SITES they cover --- 7 rows 7 sites ambiguous_unknown 25 rows 25 sites boundary_lib + 8 rows 8 sites callback_registered --- edge rows by call kind --- 7 DECORATOR_APPLICATION 8 DECORATOR_CALL + 8 METHOD_REFERENCE 17 SIMPLE_CALL --- unresolved reasons --- @@ -18,7 +20,7 @@ distinct call sites emitted: 32 25 boundary_lib --- reconciling rows against the conserved site count --- - edge rows 32 + edge rows 40 minus extra rows from multi-target sites 0 - = tier/site pairs 32 + = tier/site pairs 40 engine's conserved site total 32 diff --git a/graph/test/python/expected/42-signal-forms.edges b/graph/test/python/expected/42-signal-forms.edges index 456f0118c..f8aa33ff1 100644 --- a/graph/test/python/expected/42-signal-forms.edges +++ b/graph/test/python/expected/42-signal-forms.edges @@ -4,6 +4,7 @@ ambiguous_unknown METHOD_CALL orders.close -> - ambiguous_unknown METHOD_CALL orders.ship -> - boundary_lib DECORATOR_CALL handlers. -> external:receiver boundary_lib SIMPLE_CALL signals. -> builtin:object.__init__ +callback_registered METHOD_REFERENCE handlers. -> handlers.on_closed known_edge DECORATOR_CALL handlers. -> handlers.remember known_edge METHOD_CALL orders.not_a_signal -> orders.Outbox.asend known_edge METHOD_CALL orders.pay -> signals.Signal.asend diff --git a/graph/test/python/expected/42-signal-forms.tiers b/graph/test/python/expected/42-signal-forms.tiers index e780b18a8..f608211b9 100644 --- a/graph/test/python/expected/42-signal-forms.tiers +++ b/graph/test/python/expected/42-signal-forms.tiers @@ -1,14 +1,16 @@ -distinct call sites emitted: 22 +distinct call sites emitted: 23 --- by tier: edge ROWS, and the distinct SITES they cover --- 8 rows 8 sites ambiguous_unknown 9 rows 9 sites boundary_lib + 1 rows 1 sites callback_registered 5 rows 5 sites known_edge --- edge rows by call kind --- 5 DECORATOR_APPLICATION 5 DECORATOR_CALL 7 METHOD_CALL + 1 METHOD_REFERENCE 5 SIMPLE_CALL --- unresolved reasons --- @@ -22,7 +24,7 @@ distinct call sites emitted: 22 5 known_edge --- reconciling rows against the conserved site count --- - edge rows 22 + edge rows 23 minus extra rows from multi-target sites 0 - = tier/site pairs 22 + = tier/site pairs 23 engine's conserved site total 22 diff --git a/graph/test/python/expected/project-name-collision.tiers b/graph/test/python/expected/project-name-collision.tiers index ee405f873..d47e460f7 100644 --- a/graph/test/python/expected/project-name-collision.tiers +++ b/graph/test/python/expected/project-name-collision.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 91 +distinct call sites emitted: 92 --- by tier: edge ROWS, and the distinct SITES they cover --- 1 rows 1 sites ambiguous_unknown 34 rows 34 sites boundary_lib + 1 rows 1 sites callback_registered 49 rows 49 sites known_edge 25 rows 7 sites multi_inferred @@ -12,6 +13,7 @@ distinct call sites emitted: 91 1 DECORATOR_BARE 1 DECORATOR_CALL 25 METHOD_CALL + 1 METHOD_REFERENCE 5 SELF_CALL 58 SIMPLE_CALL 6 SUPER_CALL @@ -27,7 +29,7 @@ distinct call sites emitted: 91 7 multi_inferred --- reconciling rows against the conserved site count --- - edge rows 109 + edge rows 110 minus extra rows from multi-target sites 18 - = tier/site pairs 91 + = tier/site pairs 92 engine's conserved site total 91 diff --git a/graph/test/python/expected/project-two-service-fastapi.tiers b/graph/test/python/expected/project-two-service-fastapi.tiers index 0b54e3f31..d50f26912 100644 --- a/graph/test/python/expected/project-two-service-fastapi.tiers +++ b/graph/test/python/expected/project-two-service-fastapi.tiers @@ -1,8 +1,9 @@ -distinct call sites emitted: 229 +distinct call sites emitted: 239 --- by tier: edge ROWS, and the distinct SITES they cover --- 10 rows 10 sites ambiguous_unknown 71 rows 71 sites boundary_lib + 10 rows 10 sites callback_registered 126 rows 126 sites known_edge 59 rows 22 sites multi_inferred @@ -12,6 +13,7 @@ distinct call sites emitted: 229 8 DECORATOR_BARE 9 DECORATOR_CALL 82 METHOD_CALL + 10 METHOD_REFERENCE 2 PROPERTY_READ 27 SELF_CALL 104 SIMPLE_CALL @@ -29,8 +31,8 @@ distinct call sites emitted: 229 22 multi_inferred --- reconciling rows against the conserved site count --- - edge rows 266 + edge rows 276 minus extra rows from multi-target sites 37 - = tier/site pairs 229 + = tier/site pairs 239 engine's conserved site total 227 of which 2 are PROPERTY_READ, an edge with no call site (README decision 5) diff --git a/graph/test/python/tools/literal_gate.py b/graph/test/python/tools/literal_gate.py index 0e7af6f71..b42121347 100644 --- a/graph/test/python/tools/literal_gate.py +++ b/graph/test/python/tools/literal_gate.py @@ -13,7 +13,9 @@ provenance "client" / "lib" -- assigned by which fact file a row is staged into, not by anything in the source. confidence tiers "known_edge", "multi_inferred", "boundary_lib", - "ambiguous_unknown" -- the engine's own output vocabulary. + "ambiguous_unknown", "callback_registered" (a method + reference, call-edge-generation/method-reference.dl) -- the + engine's own output vocabulary. diagnostic labels "no_rule", "escape_hatch", ... These are OUTPUT arguments of call_unresolvable / site_reason / expr_type_untypable and are never matched against input, so they cannot bias resolution. @@ -48,7 +50,7 @@ raise SystemExit(f"literal gate: engine dir not found at {ENGINE} -- refusing to pass vacuously") PROVENANCE = {"client", "lib", "external", "builtin"} -TIERS = {"known_edge", "multi_inferred", "boundary_lib", "ambiguous_unknown"} +TIERS = {"known_edge", "multi_inferred", "boundary_lib", "ambiguous_unknown", "callback_registered"} SCAFFOLD = {"", "-", ".", "/", "true", "false", "lib:", "builtin:", "external:", "untyped_receiver:", "builtin:object.__init__", "_total_sites"} # "/" joins "." above: both are separators fixed by a specification (a dotted name, a URL diff --git a/graph/test/python/torture/expected/torture.edges b/graph/test/python/torture/expected/torture.edges index 557d3208c..54c66e115 100644 --- a/graph/test/python/torture/expected/torture.edges +++ b/graph/test/python/torture/expected/torture.edges @@ -350,6 +350,11 @@ boundary_lib SUBSCRIPT_CALL f30_builtin_elements.an_annotated_local -> builtin:s boundary_lib SUBSCRIPT_CALL f30_builtin_elements.annotated_parameter_indexed -> builtin:str.upper boundary_lib SUBSCRIPT_CALL f30_builtin_elements.through_any_object -> builtin:str.upper boundary_lib SUPER_CALL f41_class_attribute_absent.ModelMeta.__new__ -> builtin:type.__new__ +callback_registered METHOD_REFERENCE f12_value_flow.method_alias_on_class -> f12_value_flow.Widget.make +callback_registered METHOD_REFERENCE f12_value_flow.method_alias_on_instance -> f12_value_flow.Widget.emit +callback_registered METHOD_REFERENCE rxfacade. -> rxaccel.boost +callback_registered METHOD_REFERENCE rxfacade. -> rxlib.fallback +callback_registered METHOD_REFERENCE rxfacade. -> rxlib.slow known_edge CHAINED_CALL f01_inheritance.client_chain -> f01_inheritance.LocalBase.kind known_edge CHAINED_CALL f01_inheritance.client_chain -> f01_inheritance.LocalLeaf.kind known_edge CHAINED_CALL f02_callables.attribute_callables -> f02_callables.HoldsCallables.run diff --git a/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py b/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py index 5aa67cc24..98764b89a 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/ax_edges.py @@ -112,7 +112,7 @@ # C#: a call the language writes for a statement: `using` -> Dispose, `foreach` -> GetEnumerator, `{ a, b }` -> Add 'using_dispose': 'call', 'foreach_enumerator': 'call', 'collection_add': 'call', # the callable is named, not called at that line — it runs when whoever took it runs it - 'ref': 'method-ref', + 'ref': 'method-ref', 'METHOD_REFERENCE': 'method-ref', # a declaration handed to a decorator, which is what wires most framework handlers up 'DECORATOR_APPLICATION': 'decorator', 'DECORATOR_ATTRIBUTE': 'decorator', 'DECORATOR_BARE': 'decorator', 'DECORATOR_CALL': 'decorator', diff --git a/tests/cases/python/method-reference-reaches-its-target/case.json b/tests/cases/python/method-reference-reaches-its-target/case.json new file mode 100644 index 000000000..d96498e7c --- /dev/null +++ b/tests/cases/python/method-reference-reaches-its-target/case.json @@ -0,0 +1,230 @@ +{ + "lang": "python", + "src": ".", + "checks": [ + { + "why": "a bound method handed to an executor as a VALUE (`loop.run_in_executor(None, svc.cancel, oid)`) is called by the executor, by no call site the graph sees. Its receiver is typed exactly as `svc.cancel()` would be, so the function that queues it reaches it, labelled as handed over, never as a call", + "run": [ + "path", + "executor_svc", + "OrderService.cancel" + ], + "want": [ + "executor_svc → OrderService.cancel: 1 of 1 target(s) reached" + ], + "avoid": [ + "the two are independent in this graph" + ] + }, + { + "why": "every resolved shape that hands cancel over is a dependent of it: a positional argument, a keyword argument, functools.partial, a receiver the function constructed itself", + "run": [ + "impact", + "OrderService.cancel", + "--limit", + "50" + ], + "want": [ + "executor_svc", + "keyword_register", + "partial_cancel", + "constructed_receiver", + "handed over as a value" + ], + "avoid": [ + "untyped_receiver", + "by_string" + ] + }, + { + "why": "control: a same-named cancel on an unrelated class gains none of OrderService.cancel's hand-offs", + "run": [ + "impact", + "LedgerService.cancel", + "--limit", + "50" + ], + "avoid": [ + "executor_svc", + "keyword_register", + "partial_cancel", + "constructed_receiver", + "handed over as a value" + ] + }, + { + "why": "and the closure continues from the hand-off: the test that drives executor_svc reaches cancel", + "run": [ + "impact", + "OrderService.cancel", + "--tests" + ], + "want": [ + "test_queue.py" + ], + "avoid": [ + "test_ledger.py" + ] + }, + { + "why": "a bound method passed as a sort key, and one read inside a lambda: the lambda is the reader", + "run": [ + "impact", + "OrderService.rank", + "--limit", + "50" + ], + "want": [ + "sort_key", + "lambda_holder" + ], + "avoid": [ + "ledger_rank" + ] + }, + { + "why": "control: a same-named method on an UNRELATED class keeps its own reader and gains none of OrderService's", + "run": [ + "impact", + "LedgerService.rank", + "--limit", + "50" + ], + "want": [ + "ledger_rank" + ], + "avoid": [ + "sort_key", + "lambda_holder" + ] + }, + { + "why": "map() over a bound method and over the method read off its CLASS (an unbound reference)", + "run": [ + "impact", + "OrderService.convert", + "--limit", + "50" + ], + "want": [ + "map_convert", + "unbound_reference" + ] + }, + { + "why": "threading.Thread(target=...), a list of callbacks, a signal handler, a view_func keyword", + "run": [ + "path", + "thread_target", + "OrderService.run_job" + ], + "want": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "a callback stored in a list literal", + "run": [ + "path", + "callback_list", + "OrderService.on_event" + ], + "want": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "a signal handler", + "run": [ + "path", + "signal_connect", + "OrderService.handle" + ], + "want": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "a view function registered by keyword", + "run": [ + "path", + "url_rule", + "OrderService.index" + ], + "want": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "self-references: submitted to the instance's own pool, used as a sort key, and returned (an escaping reference)", + "run": [ + "path", + "Worker.submit_bound_self", + "Worker._do_work" + ], + "want": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "a self method as a sort key", + "run": [ + "path", + "Worker.sort_by_self_key", + "Worker._rank" + ], + "want": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "a returned bound method escapes to whoever called escapes()", + "run": [ + "path", + "Worker.escapes", + "Worker._handle" + ], + "want": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "a plain module function handed to a pool is now the engine's resolved reference, not a name match", + "run": [ + "path", + "submit_plain_function", + "top_level_job" + ], + "want": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "control: an UNTYPED receiver's reference resolves to nothing, exactly like `thing.cancel()`, so there is no edge to OrderService.cancel", + "run": [ + "path", + "untyped_receiver", + "OrderService.cancel" + ], + "expect_error": true, + "avoid": [ + "1 of 1 target(s) reached" + ] + }, + { + "why": "control: the method's name in a string, an attribute read OFF the method object (`svc.audit.__name__`), str() of it, and an identity test on it do not run it", + "run": [ + "impact", + "OrderService.audit", + "--limit", + "50" + ], + "avoid": [ + "by_string", + "reads_its_name", + "prints_it", + "compares_it" + ] + } + ] +} diff --git a/tests/cases/python/method-reference-reaches-its-target/orders.py b/tests/cases/python/method-reference-reaches-its-target/orders.py new file mode 100644 index 000000000..44a34d5f0 --- /dev/null +++ b/tests/cases/python/method-reference-reaches-its-target/orders.py @@ -0,0 +1,163 @@ +"""Methods and functions handed over as REFERENCES: named as a value, called later by whoever holds them.""" +import functools +import signal +import threading +from concurrent.futures import ThreadPoolExecutor + + +class OrderService: + def cancel(self, oid): + return oid + + def rank(self, x): + return x + + def convert(self, row): + return row + + def on_event(self, e): + return e + + def handle(self, sig, frame): + return sig + + def run_job(self): + return 1 + + def index(self): + return "ok" + + def audit(self, oid): + return oid + + +class LedgerService: + """Same method names as OrderService, and no relation to it.""" + + def cancel(self, oid): + return oid + + def rank(self, x): + return x + + +def top_level_job(item): + return item + + +class Worker: + def __init__(self): + self.pool = ThreadPoolExecutor() + + def _do_work(self, item): + return item + + def _rank(self, x): + return x + + def _handle(self, e): + return e + + def submit_bound_self(self, item): + return self.pool.submit(self._do_work, item) + + def sort_by_self_key(self, xs): + return sorted(xs, key=self._rank) + + def escapes(self): + return self._handle + + +def submit_plain_function(pool, item): + return pool.submit(top_level_job, item) + + +def executor_svc(loop, svc: OrderService, oid): + return loop.run_in_executor(None, svc.cancel, oid) + + +def thread_target(svc: OrderService): + t = threading.Thread(target=svc.run_job) + t.start() + return t + + +def callback_list(handler: OrderService): + callbacks = [handler.on_event] + return callbacks + + +def register(handler): + return handler + + +def keyword_register(svc: OrderService): + return register(handler=svc.cancel) + + +def sort_key(svc: OrderService, xs): + return sorted(xs, key=svc.rank) + + +def map_convert(svc: OrderService, rows): + return list(map(svc.convert, rows)) + + +def unbound_reference(rows): + return list(map(OrderService.convert, rows)) + + +def partial_cancel(svc: OrderService, oid): + return functools.partial(svc.cancel, oid) + + +def signal_connect(svc: OrderService): + signal.signal(signal.SIGTERM, svc.handle) + + +def url_rule(app, views: OrderService): + app.add_url_rule("/x", view_func=views.index) + + +def constructed_receiver(oid): + svc = OrderService() + return functools.partial(svc.cancel, oid) + + +def lambda_holder(svc: OrderService, xs): + return sorted(xs, key=lambda x: svc.rank) + + +# ── near-miss controls ─────────────────────────────────────────────────────── +def untyped_receiver(executor, thing, oid): + """`thing` has no type: `thing.cancel` may be any class's cancel, so it resolves to none.""" + return executor.submit(thing.cancel, oid) + + +def ledger_rank(ledger: LedgerService, xs): + """LedgerService.rank, not OrderService.rank.""" + return sorted(xs, key=ledger.rank) + + +def by_string(svc: OrderService): + """The method's NAME in a string is not a reference to it.""" + return getattr_name("audit") + + +def getattr_name(n): + return n + + +def reads_its_name(svc: OrderService): + """Reading an attribute OF the method object does not run it.""" + return svc.audit.__name__ + + +def prints_it(svc: OrderService): + """str() of the method object runs the function object's __str__, not the method.""" + return str(svc.audit) + + +def compares_it(svc: OrderService, cb): + """An identity test on the method object does not run it.""" + return cb is svc.audit diff --git a/tests/cases/python/method-reference-reaches-its-target/tests/test_ledger.py b/tests/cases/python/method-reference-reaches-its-target/tests/test_ledger.py new file mode 100644 index 000000000..5ba83160c --- /dev/null +++ b/tests/cases/python/method-reference-reaches-its-target/tests/test_ledger.py @@ -0,0 +1,5 @@ +from orders import LedgerService, ledger_rank + + +def test_ledger_rank(): + assert ledger_rank(LedgerService(), [2, 1]) == [1, 2] diff --git a/tests/cases/python/method-reference-reaches-its-target/tests/test_queue.py b/tests/cases/python/method-reference-reaches-its-target/tests/test_queue.py new file mode 100644 index 000000000..8d8de3896 --- /dev/null +++ b/tests/cases/python/method-reference-reaches-its-target/tests/test_queue.py @@ -0,0 +1,9 @@ +from unittest import mock + +from orders import OrderService, executor_svc + + +def test_queues_the_cancel(): + loop = mock.Mock() + executor_svc(loop, OrderService(), "o-1") + loop.run_in_executor.assert_called_once()