Skip to content

Intra-class method calls resolve to the class node: jedi inference at ast.Call col_offset hits the receiver, not the callee #94

Description

@rahlk

PROBLEM

For an intra-class method call (self._method(...)), the emitted call graph points the edge at the CLASS node instead of the callee method — and the callee method may not appear as a graph node at all. The symbol table is correct (the method exists as a PyCallable with the right signature); only the call-site/edge resolution is wrong. This silently truncates call chains at their deepest hop (the codellm-devkit/python-sdk#65 symptom: web_auth_reset_password -> ResUsers.reset_password -> _action_reset_password loses the last hop).

MINIMAL REPRO (verified against installed 0.3.0)

# pkg/mod.pyclassResUsers:
defreset_password(self, login):
returnself._action_reset_password([login])
def_action_reset_password(self, ids):
returnidsdefentry():
returnResUsers().reset_password("x")

Raw Codeanalyzer.analyze() output (no SDK involved):

source='pkg.mod.entry' target='pkg.mod.ResUsers.__init__' type='CALL_DEP' weight=2 provenance=['jedi']
source='pkg.mod.ResUsers.reset_password' target='pkg.mod.ResUsers' type='CALL_DEP' weight=1 provenance=['jedi']

Expected second edge target: pkg.mod.ResUsers._action_reset_password.

Raw call-site record for the intra-class call — callee_signature is the class:

{"method_name":"_action_reset_password","receiver_expr":"self","receiver_type":"ResUsers",
"callee_signature":"pkg.mod.ResUsers","is_constructor_call":false,"start_line":3}

ROOT CAUSE (localized in 0.3.0)

codeanalyzer/syntactic_analysis/symbol_table_builder.py, _call_sites (~lines 590-594): _infer_callee(script, node.lineno, node.col_offset) is called with the ast.Call node's position. For attribute calls (self.method(...)), node.col_offset is the column of the RECEIVER (self), not the method name — so Jedi infers the receiver's type, whose full_name collapses to the class's qualified name. _infer_callee's constructor-rewrite guard (d.type == "class", ~line 92) does not fire because an instance's d.type is "instance", so the class name passes through unrewritten. Bare-name calls (ResUsers()) work only because the call's col_offset coincides with the callee expression there.

Likely fix direction: for ast.Attribute callees, infer at the attribute NAME's position (node.func.value.end_col_offset + 1 / node.func.attr position), or resolve via the attribute node explicitly.

DOWNSTREAM / PROPAGATION

  • The SDK (python-sdk) is a verbatim pass-through (verified: _build_call_graph and get_callsites_for forward analyzer output unmodified) — no SDK-side fix applies.
  • python-sdk 1.x is pinned to 0.3.0: fixing this for 1.x users needs a 0.3.1 backport, or ships naturally with 0.4.0 (schema v2) if the v2 emission path inherits the same _call_sites inference — PLEASE VERIFY the v2 branch against this repro before cutting 0.4.0; the L1/L2 jedi path likely shares the code, and the superset gate would then bake the wrong edge into v2 goldens.
  • Suggested regression gates: (1) edge reset_password -> ResUsers._action_reset_password exists; (2) NO callable -> class edge for a non-constructor call; (3) callsite callee_signature for self.x(...) ends with .x.

Reported from a python-sdk audit session; full repro scripts available on request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions