Repository navigation
Description
Target: v1 schema /
main(schema 1.1.0 — the lineage the audit measured and what v0.3.0 shipped). The same defect propagates into the canonical schema v2 pipeline; that side is tracked in #87, since the v2 chain consumescallee_signaturethrough the L2 backfill.
Summary
An external quality audit of the v0.3.0 Neo4j export (run against Odoo, schema 1.1.0) surfaced two Critical findings — CLDK-001 (call binding) and CLDK-002 (Odoo env[...] dispatch). They share one root cause, so they are filed together: callee inference is anchored at the start of the call expression, so every attribute call resolves to its receiver's type instead of the invoked method.
Audit evidence (Odoo, native export)
- 52,483 of 59,523 resolved callsites have
PY_RESOLVES_TOtargets whosenamediffers from the callsite'smethod_name. - Target breakdown: 30,772
:PyClass, 27,168:PyExternal, 1,583:PyCallable. In 8,320 of the class-target cases the receiver class has a method with the exactmethod_name— the correct target was onePY_HAS_METHODhop away. envdispatch special case: of 7,396 resolved callsites whose receiver contains.env[(e.g.self.env['account.account'].search(...)), 7,391 (99.93%) resolve to the caller's own enclosing class.
Root cause
_infer_callee (and the adjacent return_type inference) in codeanalyzer/syntactic_analysis/symbol_table_builder.py anchor Jedi at the ast.Call node's own position:
callee_signature, is_constructor = self._infer_callee(
script, node.lineno, node.col_offset
)
return_type = self._infer_type(script, node.lineno, node.col_offset)(main @ symbol_table_builder.py:591-594)
An ast.Call's col_offset is the first character of the whole call expression. For any attribute call receiver.method(...) that is the receiver token, so script.infer() returns the receiver's type:
self.helper()→ Jedi infersself→ the enclosing class →PY_RESOLVES_TOthe:PyClassnode (name mismatch:helpervs class name).self.env['account.account'].search([])→ Jedi infersself→ enclosing class. This is the entire CLDK-002 artifact: noenvsemantics are involved, the anchor never gets pastself.- Receivers typed to stdlib/third-party become the 27,168
:PyExternalghost targets. - Only bare-name calls (
len(...),foo()) anchor on the callee by coincidence — the 7,040 name matches.
Minimal reproduction
mini_odoo.py in an empty project dir:
class Environment:
def __getitem__(self, name):
return Model()
class Model:
env = Environment()
def search(self, domain): return []
def helper(self): return 42
class AccountMove(Model):
def action_post(self):
accounts = self.env['account.account'].search([])
self.helper()
return str(len(accounts))b = SymbolTableBuilder(fixture_dir, None)
mod = b.build_pymodule_from_file(fixture_dir / "mini_odoo.py")Observed callsites in AccountMove.action_post:
method_name='search' receiver_expr="self.env['account.account']" callee_signature='mini_odoo.AccountMove'
method_name='helper' receiver_expr='self' callee_signature='mini_odoo.AccountMove'
method_name='str' callee_signature='builtins.str.__init__' (correct — bare name)
method_name='len' callee_signature='builtins.len' (correct — bare name)
Expected: search → mini_odoo.Model.search, helper → mini_odoo.Model.helper.
Fix direction
For ast.Attribute callees, anchor Jedi at the attribute name's position (within .attr, i.e. derived from func_expr.value.end_lineno / end_col_offset), not the Call node's start. Keep the existing class→__init__ constructor normalization. return_type inference at the same wrong anchor should move with it. Add the fixture above as a regression test; the audit's acceptance gate is ≥95% name equality for resolvable non-constructor calls, with class fallback prohibited when the exact method exists.
Note on CLDK-002 scope: this fix removes the actively-wrong enclosing-class binding. Resolving env['model'] through Odoo's registry to the right model class is framework semantics (model identity/composition, #83) and stays a separate design-level work item — after this fix those calls should either resolve through the receiver's static type or stay unresolved, never bind to the lexical caller.
Activity
fix(schema): strip call_sites at emit, not with a field exclude (#120)