Summary
_resolve_cpp_member_calls types a member-call receiver from cpp_type_table, which is populated only from local variable declarations. Function parameters are never entered into it, so any call made through a parameter receiver is silently skipped.
TS/JS have an explicit augmentation pass for exactly this — _ts_receiver_type_table (extractors/engine.py:6008), whose comment states it adds "local new bindings and type-annotated parameters". C++ has no equivalent, so its table gets local declarations only.
This matters disproportionately for C++, where passing state by const& / * and calling through it is the dominant idiom.
Reproduction
thing.h:
#pragma once
namespace NS {
class Thing {
public:
bool IsOk() const { return true; }
};
}
use.cpp:
#include "thing.h"
using namespace NS;
bool Local() { Thing t; return t.IsOk(); } // local var
bool ParamRef(const Thing& T) { return T.IsOk(); } // param by ref
bool ParamPtr(Thing* P) { return P->IsOk(); } // param by ptr
bool ParamVal(Thing V) { return V.IsOk(); } // param by value
from pathlib import Path
from graphify.extract import extract, collect_files
r = extract(collect_files(Path('.')), cache_root=Path('.'))
Actual
| receiver form |
calls edge |
Thing t; t.IsOk() — local variable |
emitted (INFERRED) |
const Thing& T — parameter by ref |
none |
Thing* P — parameter by pointer |
none |
Thing V — parameter by value |
none |
B.Inner.IsOk() — chained receiver |
none |
Inner.IsOk() — member field receiver |
none |
For contrast, the paths that do work: bare intra-class calls (Own()), this->Own(), and Foo::bar() all resolve correctly — so this is specifically about the cpp_type_table typing source, not member-call handling in general.
Expected
A parameter with an explicit type annotation is at least as strong a typing source as a local declaration — the type is written directly in the signature, with no inference required. It should populate cpp_type_table and resolve, subject to the same single-definition god-node guard already in place.
The same argument applies to class fields with declared types (Thing Inner; then Inner.IsOk()), which are also absent from the table. Chained receivers (B.Inner.IsOk()) are documented as a deliberate bail and are out of scope here.
Impact measured on a real codebase
Unreal Engine 5 C++ project, 153 files, .cpp/.h:
- distinct caller→callee pairs resolvable to an in-repo definition: 1,182
- free / namespace-qualified: 696
- member-only (never also called as a free function): 486 (41.1%)
- member-only pairs present in
graph.json: 0 of 486
Nearly every receiver in that codebase is a parameter (ResolveTurn(const FSimState& State, ...) then State.IsValidPlayer(...)) or a chained expression (State.GalaxyMap.AreAdjacent(...)), so the local-declaration table never applies.
Downstream, this inflates the "weakly-connected nodes" count in GRAPH_REPORT.md (586 in this build) and skews community detection, since intra-module coupling is under-represented by ~41%.
Notes
I understand the deliberate precision-over-recall stance in the docstring ("a false call edge is worse than a missing one", god-node guard). This report isn't asking to relax that — an explicitly typed parameter carries the same evidence strength as the local declarations already accepted, and would keep the single-definition guard intact.
Related: #1547 (introduced the C++ resolver), #2234 (Rust has no member-call pass at all), #2860 (Python self.<attr> receivers), #2561 (Swift attribute-typed properties). Same class of gap, different language.
Environment
- graphify 0.9.52 (
uv tool install graphifyy)
- Python 3.10.21, macOS arm64
Summary
_resolve_cpp_member_callstypes a member-call receiver fromcpp_type_table, which is populated only from local variable declarations. Function parameters are never entered into it, so any call made through a parameter receiver is silently skipped.TS/JS have an explicit augmentation pass for exactly this —
_ts_receiver_type_table(extractors/engine.py:6008), whose comment states it adds "localnewbindings and type-annotated parameters". C++ has no equivalent, so its table gets local declarations only.This matters disproportionately for C++, where passing state by
const&/*and calling through it is the dominant idiom.Reproduction
thing.h:use.cpp:Actual
callsedgeThing t; t.IsOk()— local variableconst Thing& T— parameter by refThing* P— parameter by pointerThing V— parameter by valueB.Inner.IsOk()— chained receiverInner.IsOk()— member field receiverFor contrast, the paths that do work: bare intra-class calls (
Own()),this->Own(), andFoo::bar()all resolve correctly — so this is specifically about thecpp_type_tabletyping source, not member-call handling in general.Expected
A parameter with an explicit type annotation is at least as strong a typing source as a local declaration — the type is written directly in the signature, with no inference required. It should populate
cpp_type_tableand resolve, subject to the same single-definition god-node guard already in place.The same argument applies to class fields with declared types (
Thing Inner;thenInner.IsOk()), which are also absent from the table. Chained receivers (B.Inner.IsOk()) are documented as a deliberate bail and are out of scope here.Impact measured on a real codebase
Unreal Engine 5 C++ project, 153 files,
.cpp/.h:graph.json: 0 of 486Nearly every receiver in that codebase is a parameter (
ResolveTurn(const FSimState& State, ...)thenState.IsValidPlayer(...)) or a chained expression (State.GalaxyMap.AreAdjacent(...)), so the local-declaration table never applies.Downstream, this inflates the "weakly-connected nodes" count in
GRAPH_REPORT.md(586 in this build) and skews community detection, since intra-module coupling is under-represented by ~41%.Notes
I understand the deliberate precision-over-recall stance in the docstring ("a false call edge is worse than a missing one", god-node guard). This report isn't asking to relax that — an explicitly typed parameter carries the same evidence strength as the local declarations already accepted, and would keep the single-definition guard intact.
Related: #1547 (introduced the C++ resolver), #2234 (Rust has no member-call pass at all), #2860 (Python
self.<attr>receivers), #2561 (Swift attribute-typed properties). Same class of gap, different language.Environment
uv tool install graphifyy)