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()