Skip to content

method calls through object instances/pointers are dropped in CALLER EDGES #1153

Closed

Description

Version

codebase-memory-mcp 0.9.0

Platform

Linux (x64)

Install channel

GitHub release archive / install.sh / install.ps1

Binary variant

standard

What happened, and what did you expect?

The dropped calls share a trait: they're method calls through object instances/pointers:

instEntry->processChange() ❌
instEntry->processAdd() ❌
instEntry->setApp() ❌
appEntry->samplerInstEntry() ❌
params->printParams() ❌
The captured calls are mostly free functions / static / uniquely-named:

gSampler.sampleInstEntry(...) ✅ (0.38, unique_name)
createPlatInstEntry(...) ✅ (free function)
insertSampleInstEntry(...) ✅

Reproduction

codebase-memory-mcp cli index_repository --repo-path /data/src/repo --mode fast --name branch --persistence true

HOME=/data/codebase codebase-memory-mcp cli query_graph --project branch --query "MATCH (caller:Method)-[c:CALLS]->(callee) WHERE caller.qualified_name = 'master.forwarding.aftman.sampler.src.SamplerMsgHandler.SamplerMsgHandler.processAdd' RETURN callee.name, callee.qualified_name, callee.file_path, c.confidence, c.candidates, c.strategy, c.line ORDER BY c.line LIMIT 50"

Logs


Diagnostics trajectory (memory / performance / leak issues)


Project scale (if relevant)

No response

Confirmations

  • I searched existing issues and this is not a duplicate.
  • My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.

Activity

  1. added
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    and removed
    windowsWindows-specific issues
    on Jul 18, 2026
  2. added this to the 0.9.1-rc milestone on Jul 18, 2026
  3. DeusData commented on Jul 18, 2026

    @DeusData
    Owner

    Thanks for the focused receiver-call evidence. I removed the incorrect Windows label and placed this in the high-priority 0.9.1-rc query/call-resolution queue under #594. The RED guard should cover C/C++ pointer and instance receivers independently, prove the enclosing Method remains the caller, and distinguish a missing edge from an edge incorrectly attributed to a File or Module node.

  4. sreehari-malapati-myprod commented on Aug 1, 2026

    @sreehari-malapati-myprod
    Author

    @DeusData ..

    Relates to / extends #1153 (method calls through object instances/pointers dropped in caller edges).

    Summary:
    A CALLS caller edge is missing for a method invoked through a static factory return — SecdManager::getInstance()->secd_retrieve_pic_fpc_i2c_id(...). The callee is non-virtual (statically bound), so the edge is resolvable from the static return type of getInstance() alone, with no polymorphic/vtable analysis. This broadens #1153 from local pointer/instance receivers to factory/accessor-return receivers (getInstance()->foo(), getX()->foo()).

    Impact:
    query_graph caller queries and trace_path --direction upstream report 0 callers for a function that clearly has one, breaking impact analysis / reverse call-graph for the very common singleton pattern.

    Symbol:
    secd_retrieve_pic_fpc_i2c_id — C++ method SecdManager::secd_retrieve_pic_fpc_i2c_id, non-virtual, is_entry_point=false
    qualified_name = master.securityd.common.app.src.SecdManager.SecdManager.secd_retrieve_pic_fpc_i2c_id

    Where it exists (OpenGrok full:secd_retrieve_pic_fpc_i2c_id, 3/3 hits):

    decl: a/b/app/src/SecdManager.h:164
    def : a/b/app/src/SecdManager.cpp:2257
    call: a/b/pil/src/SecdCfgHandlerThread.cpp:237
    Where it's called:
    Inside GetExternalPhyPresent(), via a singleton factory return:
    // securityd/common/pil/src/SecdCfgHandlerThread.cpp:237
    if(!SecdManager::getInstance()->secd_retrieve_pic_fpc_i2c_id(ifdh, fpc_id, pic_id)) {
    }

    How it was missed:
    query_graph — caller query returns 0 (edge never created):
    MATCH (caller)-[r:CALLS]->(f:Method)
    WHERE f.name='secd_retrieve_pic_fpc_i2c_id' AND f.file_path CONTAINS 'SecdManager.cpp'
    RETURN caller.name, caller.file_path, r.line -> {"rows":[],"total":0}
    Also 0 with exact qualified_name, with (caller:Method), and via USAGE-incoming.

    trace_path — --direction upstream (and both) reports 0 callers / in_degree=0 for the same function.

    Proof it's a real drop, not a bad query:
    The mirror callee direction, same anchor, returns 9 rows -> the anchor resolves fine; only the incoming edge is missing:
    MATCH (f:Method)-[r:CALLS]->(c)
    WHERE f.name='secd_retrieve_pic_fpc_i2c_id' AND f.file_path CONTAINS 'SecdManager.cpp'
    RETURN c.name, r.line ORDER BY r.line -> 9 rows (getFpc, getPic, fpc_map, ifdo_hndl, ...)

    Expected:
    A CALLS edge GetExternalPhyPresent -> SecdManager::secd_retrieve_pic_fpc_i2c_id. Both the caller query and trace_path --direction upstream should list GetExternalPhyPresent (SecdCfgHandlerThread.cpp:237).

    Fix scope:
    Receiver is a factory-return (getInstance()->) and callee is non-virtual -> resolvable from the static return type alone (no vtable analysis). Fix should cover factory()->method() / accessor()->method() receivers, not only local pointer/instance variables.

    can you please let me know when can we expexct 0.9.1-rc

  5. Limebird01 commented on Aug 1, 2026

    @Limebird01

    Confirming this also reproduces for Python instance-variable method calls on v0.9.0 (linux-amd64, portable static binary from the GitHub release archive), so it is not limited to C/C++ pointer receivers.

    Minimal dummy repro:

    # stack.py
    class StackManager:
        def status(self, verbose=False):
            return "ok"
    
    # main.py
    from stack import StackManager
    
    manager = StackManager()
    manager.status(verbose=True)

    Observed on a mid-size Python repo indexed with --mode full:

    • trace_path on StackManager.status with --direction upstream reports callers=0, despite the real call site through the instance variable.
    • The callee direction (--direction downstream) is accurate, including cross-file — so the anchor resolves fine and only the incoming CALLS edge is missing, matching the reports above.

    So the RED guard for the 0.9.1-rc fix should cover Python instance receivers (obj = Cls(); obj.method()) in addition to the C/C++ pointer and factory-return cases.

  6. DeusData commented on Sep 19, 2026

    @DeusData
    Owner

    Thank you for the additional C++ factory-return and Python instance-call examples. They broaden the receiver forms that a fix must cover. v0.11.0 is now published, but I have not verified that these exact cases are fixed there, so I cannot offer a fix-version promise or close this based on release age.

  7. BigBrotherHui commented on Sep 21, 2026

    @BigBrotherHui

    Answering your 2026-09-19 question about v0.11.0 directly: no, these cases are not fixed in v0.11.0 — and I have the root cause.

    I reproduced on the v0.11.0 release binary (codebase-memory-mcp 0.11.0, windows-amd64, official archive, install.ps1 + checksum verified), clean mode=full index, and cross-checked the same case on 0.10.8.

    Root cause: an export macro between class and the class name demotes the class to a Function node

    // inc/worker.h
    class MYMODULE_EXPORT Worker {      // <-- macro here
    public:
      bool isActive() const;
    };
    
    // inc/worker.cpp
    bool Worker::isActive() const { return true; }
    
    // ctl/controller.cpp
    #include "worker.h"
    bool callIt() {
      Worker *w = nullptr;
      w->isActive();                    // <-- CALLS edge never created
      return true;
    }

    Observed:

    MATCH (n) WHERE n.name='Worker'
      ["Function"]  fdF.inc.worker.Worker   inc/worker.h      <-- should be Class
      ["Function"]  fdF.ctl.worker.Worker   ctl/worker.h
    
    MATCH (a)-[r:DEFINES_METHOD]->(b)
      n = 0                                                   <-- no class->method ownership at all
    

    Remove the macro and the same tree produces Class + DEFINES_METHOD + both CALLS edges:

    variant worker.h declares Worker node label w->isActive() edge
    no macro class Worker { Class lsp_base_dispatch
    macro class MYMODULE_EXPORT Worker { Function dropped

    Identical with and without a base clause (class MYMODULE_EXPORT Worker { vs ... : public QObject {), and with and without a #include of the defining header, so the macro is the whole discriminator. This is extract_defs.c (class-declaration recognition), not c_lsp.c call resolution: the LSP side never gets a type to resolve, because no Class node exists and DEFINES_METHOD is empty.

    The forward declaration is a secondary symptom, not the cause

    While narrowing this I initially blamed forward declarations, because the misattributed Class node you can see in a real repo usually sits at a forward decl. The actual sequence is:

    1. the real definition is demoted to Function (macro), and
    2. the only Class node that survives is the one minted from class X; in some other header,

    so ptr->method() resolves against a type qualified name under the wrong file prefix and finds no member. Fixing (1) removes the observable symptom; a separate guard for (2) would be worth having.

    Receivers confirmed to work, so the guard does not need to cover them

    Same tree, same index, only the receiver form varies:

    receiver form result
    this->ensureWorker() lsp_type_dispatch 0.95
    Worker *w = this->ensureWorker(); w->isActive(); (local pointer) lsp_base_dispatch
    Worker stack; stack.isActive(); (stack object) resolves
    Worker *fresh = new Worker(); fresh->isActive(); (heap) resolves
    #include "worker.h" in the caller's header, no macro resolves
    any of the above, with the export macro on the class dropped

    Local pointers, factory/accessor returns, and Python instance receivers (the case in the 2026-08-01 comment, id 5149866495) all resolve here. The macro is what breaks them.

    Real-repo impact

    Modules/Planning/Registration/YSRegistration, a ~1,200-file C++/Python repo:

    • qSlicerYSRegistrationModuleWidget.cxx has 17 workflow-> member calls across 11 distinct targets, and not one of those 17 calls produced an edge. The file does contribute 28 CALLS edges, but every one of them targets a different symbol; the only inbound edges those 11 targets have come from other files. So 100% of that file's pointer member-call edges are missing.
    • qSlicerOralScanWorkflow: Class node lives at qSlicerYSRegistrationModuleWidget.h (forward decl), while its methods live under ...YSRegistration.Widgets.qSlicerOralScanWorkflow.…. Different QN prefixes, so trace_path --direction inbound on isActive / showExistingFusionResult returns callers_total: 0 — a false dead-code signal.
    • 188 class definitions in the repo take a *_EXPORT macro between class and the name; each one is a latent instance.
    • Whole-repo scan: of 1,181 Class nodes, 50 point at a file that only forward-declares the name while the real definition is elsewhere (vtkSlicerImplantLogic, vtkMRMLMarkupsCrownNode, vtkMRMLMarkupsImplantNode, vtkSlicerYSRegistrationLogic, vtkMRMLMarkupsAimingDisplayNode, …).

    Not related to #220 — I checked the edge source label distribution and it is 100% Method, so these edges are genuinely absent, not re-attributed to a File/Module node.

    Suggested RED guards

    1. A class declaration whose head is class <IDENT> <NAME> (i.e. an uppercase macro token between the keyword and the declared name) still yields Class + DEFINES_METHOD + resolvable member calls.
    2. A class X; forward declaration in file A never mints the Class node when a real definition of X exists anywhere in the index; the definition wins.
    3. Both, with the caller body unchanged, so the failure isolates to declaration extraction rather than call resolution.

    Happy to open a PR if a pointer to the right spot in extract_defs.c would help — but the RED test above is probably the higher-value half.

    (The three-file repro above is self-contained — I can paste the exact tree, or the wider A/B matrix I used to isolate the macro, if either would help.)

  8. DeusData commented on Sep 21, 2026

    @DeusData
    Owner

    @BigBrotherHui — thank you, that root cause is exactly right, and it arrived on the same day as its fix. An export macro between class and the class name made the extractor read class MYMODULE_EXPORT Worker { as a function returning class MYMODULE_EXPORT, so Worker became a Function node and w->isActive() had no class to dispatch through. That is the defect #2005 (issue #1989, by @XIYBHK) fixes, and it is now on main as c7488b0.

    I ran your three files, plus a control copy without the macro, through main before the fix and through the fix:

    binary fixture Worker CALLS from callIt edges
    before with macro Function (return_type: "class MYMODULE_EXPORT") none — only a USAGE -> Function Worker 15
    before control Class -> Worker.isActive (lsp_base_dispatch, 0.95) 16
    after with macro Class -> Worker.isActive (lsp_base_dispatch, 0.95) 16
    after control Class same 16

    With the fix, the macro fixture's graph is identical to the control's — same labels, same edge properties, same totals. It ships in the next patch release; until then a build from main has it.

    Two honest limits: this is one macro shape (a bare identifier between class and the name — the conventional export-macro form the fix targets; macros with arguments, e.g. EXPORT(x), are not covered by the zero-config rule and are the follow-up tracked on #1989), and this issue's original report listed more than one pattern. @sreehari-malapati-myprod — could you re-check your original cases against a build from main? If they all resolve, I will close this; if one does not, that one is a second cause and deserves its own reproduction here.

  9. DeusData commented on Sep 26, 2026

    @DeusData
    Owner

    Thank you, @sreehari-malapati-myprod, @Limebird01 and @BigBrotherHui! The export-macro fix is on main (c7488b0) and ships in the next patch release.

    Retesting the other shapes on main, local pointers, heap and stack objects, and in-function Python instances all resolve. Two shapes still drop the edge:

    • A call through a static factory return, SecdManager::getInstance()->retrieve(…): getInstance is linked, retrieve is not.
    • A Python instance call at module level, manager = StackManager(); manager.status(): only the constructor is linked. The same call inside a function resolves.

    We'll track these two here.

  10. DeusData commented on Oct 1, 2026

    @DeusData
    Owner

    Thank you again, @sreehari-malapati-myprod, @Limebird01 and @BigBrotherHui. Your reports pinned this down precisely.

    The C++ static-factory case is fixed in #2466, and the cause was more specific than "factory returns":

    • The cross-file C++ resolver looked up SecdManager::getInstance only under the calling file's own module.
    • A class declared in a header and defined in a .cpp lives under that header's module, so the static call was never resolved through its declared type.
    • Its return type stayed unknown, so the ->secd_retrieve_pic_fpc_i2c_id(...) call after it had nothing to dispatch on.
    • In a single file the same code already worked, which is why this was easy to miss.

    The fix resolves the class across files, but only when exactly one class carries that name. An ambiguous name stays unresolved rather than being guessed. It covers raw pointers, references, std::unique_ptr and std::shared_ptr returns. On qBittorrent it adds 489 correct edges, mostly Preferences::instance()->….

    The Python module-level case (manager = StackManager(); manager.status()) has a different cause: module-level calls never joined their type resolution. That is fixed in a separate Python change. I'll post here when both are on main.

    One honest limit: namespaced singletons such as RSS::Session::instance()->… still drop, because forward declarations currently create extra class nodes. That needs its own fix, and I'll track it separately.

  11. added a commit that references this issue on Oct 4, 2026
  12. DeusData commented on Oct 17, 2026

    @DeusData
    Owner

    Thank you again, @sreehari-malapati-myprod, @Limebird01 and @BigBrotherHui. Your reports, and the way you narrowed them down to the exact receiver shapes, are what made these fixes possible.

    Both remaining shapes are now fixed on main and shipped in v0.12.0:

    • C++ static-factory returns (SecdManager::getInstance()->retrieve(…)): fixed in cdd8c89 (fix(cpp): resolve Class::member calls across files (#1153) #2466). The cross-file C++ resolver now finds Class::member through a class declared in another file's header, as long as exactly one class carries that name. The returned type is therefore known, and the member call after it gets its CALLS edge.
    • Python instance calls at module level (manager = StackManager(); manager.status()): fixed in bf5ea5a. Module-level calls now join their LSP resolution in both pipeline modes, just as calls inside functions already did.

    Together with the export-macro fix from #2005 (c7488b0, thanks to @XIYBHK), every shape reported here is covered, so I'm closing this issue.

    One honest limit hasn't changed since my last update: namespaced singletons such as RSS::Session::instance()->… can still lose the edge, because a forward declaration currently creates an extra class node. That belongs with the C++ forward-declaration work (#1970), not here.

    If any of your original cases still misses an edge on v0.12.0, please comment here and I'll reopen it.

  13. sreehari-malapati-myprod commented on Oct 23, 2026

    @sreehari-malapati-myprod
    Author

    Direct same-file calls to static functions produce zero CALLS edges (empty trace_path callees) -its basic feature issue.

    For a C function that makes direct, same-translation-unit calls to static helper functions, the indexer creates no CALLS edges. trace_path --direction outbound returns an empty callees list even though the source clearly contains the calls. Reproduced on both v0.9.x and v0.12.1.

    Environment
    cbm versions: 0.9.x and 0.12.1 (same result)
    Host: Linux, --mode full --persistence true

    Source evidence (calls are unambiguous in the body) :
    int s3ip_sysfs_psu_drivers_register(struct s3ip_sysfs_psu_drivers_s *drv)
    {
    ...
    ret = psu_root_create(); // direct call
    if (ret < 0) { ...; return ret; }

    ret = psu_sub_create();           // direct call
    if (ret < 0) { psu_root_remove(); ...; return ret; }   // direct call
    
    ret = psu_temp_create();          // direct call
    if (ret < 0) { psu_sub_remove(); psu_root_remove(); ...; return ret; }  // direct calls
    ...
    

    }

    The callees are static functions defined in the same file,:

    static void psu_root_remove(void) { ... } // line 914
    static void psu_sub_remove(void) { ... } // line 881

    prd3://data/codebase/.local/bin# HOME=/data/codebase ./codebase-memory-mcp cli trace_path
    --project 25.2.branch
    --function-name " 25.2.branch.platform.s3ip-sysfs.s3ip_sysfs_frame.psu_sysfs.s3ip_sysfs_psu_drivers_register"
    --direction outbound --depth 1
    level=info msg=mem.init budget_mb=192775 total_ram_mb=385550
    {"function":" 25.2.branch.platform.s3ip-sysfs.s3ip_sysfs_frame.psu_sysfs.s3ip_sysfs_psu_drivers_register","direction":"outbound","callees":[]}

    prd3://data/codebase/.local/bin# HOME=/data/codebase ./codebase-memory-mcp cli trace_path --project 25.2.branch --function-name " 25.2.branch.platform.s3ip-sysfs.s3ip_sysfs_frame.psu_sysfs.s3ip_sysfs_psu_drivers_register" --direction both --depth 1
    level=info msg=mem.init budget_mb=192775 total_ram_mb=385550
    {"function":"25.2.branch.platform.s3ip-sysfs.s3ip_sysfs_frame.psu_sysfs.s3ip_sysfs_psu_drivers_register","direction":"both","callees":[],"callers":[]}

    Root cause (hypothesis)
    Because each static helper name has multiple identical copies across the repo, the call resolver cannot prove a unique global target and drops the edge. Per the README, ambiguous targets are retained as USAGE — but here even USAGE-to-callee is absent, so the call is dropped entirely. The resolver appears to skip same-file (translation-unit) scoping: a static callee should resolve to the local definition in the same file before the global uniqueness check.

    Suggested fix
    For C/C++, resolve static (internal-linkage) calls against same-file definitions first, so a duplicated static name resolves to its local translation-unit target and emits a CALLS edge, instead of being dropped as ambiguous.

    Impact
    Any C file with static helpers whose names are duplicated elsewhere in the repo loses its intra-file call graph — trace_path, call-hierarchy, and impact analysis silently return empty results (false negatives), which is worse than an error because it looks like "no call

  14. sreehari-malapati-myprod commented on Oct 23, 2026

    @sreehari-malapati-myprod
    Author

    in 0.12.1 -> more parsing errors in latrest release many files are skipped with partial parsing

    codebase-memory-mcp index coverage report

    project=25.2.branch skipped=3354 parse_partial=3715

    in 0.9.

    codebase-memory-mcp index skip report

    project=25.2.branch skipped=2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions