Skip to content

C++: member calls on parameter and field receivers are dropped — cpp_type_table is built from local declarations only (no _ts_receiver_type_table equivalent) #3215

Description

@rondss

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions