Skip to content

C++: indirect include plus forward declaration binds one receiver to unrelated duplicate classes #2542

Description

@knewstimek

Version / platform

Source build on Windows x64, MCP server version dev. Upstream base: 96c3f41cf334d87670cb085f1fcf16f637293222; tested with the narrow direct-include fix in #2541, head 7d3e09baf8b132a140ded36443382a8b54530986. This uses the upstream executable, not a reduced fork. Tested binary SHA-256: f932d8fdb7deb0c19f58bb94b1c6cf914ca1c43a565e705e5b7176ed1f410cd5.

Problem

A valid C++ caller obtains ProbeDevice through an indirect include and then includes a header with a forward declaration of that same class. The graph connects RunProbe to methods from two unrelated same-named classes that are not included in this translation unit. Adding a compilation database with the intended header search path does not prevent these wrong edges in this fixture.

This reports a concrete wrong-target result. It does not assume compiler-equivalent analysis is a promised feature, or establish header-search-path handling as the root cause.

Self-contained synthetic reproduction

Save this as make_repro.py and run python make_repro.py. All identifiers and files below are independently constructed synthetic examples.

from pathlib import Path
import json

base = Path("cpp-indirect-repro")
for case in ("quoted_indirect_forward", "search_path_indirect_forward", "direct_control"):
    repo = base / case
    repo.mkdir(parents=True, exist_ok=True)
    for folder, methods in (("alpha", ("Open", "Close")),
                            ("beta", ("Read", "Write")),
                            ("gamma", ("Open", "Close", "Read", "Write"))):
        (repo / folder).mkdir(exist_ok=True)
        code = "#pragma once\nclass ProbeDevice { public: "
        code += " ".join("int " + m + "() { return 0; }" for m in methods)
        (repo / folder / "probe_device.h").write_text(code + " };\n", encoding="utf-8")
    (repo / "decls.h").write_text("class ProbeDevice;\n", encoding="utf-8")
    bridge = ('#include <probe_device.h>\n' if case.startswith("search")
              else '#include "gamma/probe_device.h"\n')
    (repo / "bridge.h").write_text(bridge, encoding="utf-8")
    includes = ('#include "gamma/probe_device.h"\n' if case == "direct_control"
                else '#include "bridge.h"\n#include "decls.h"\n')
    calls = "void RunProbe(ProbeDevice& dev) { dev.Open(); dev.Close(); dev.Read(); dev.Write(); }\n"
    (repo / "caller.cpp").write_text(includes + calls, encoding="utf-8")
    if case.startswith("search"):
        command = {"directory": str(repo.resolve()),
                   "file": str((repo / "caller.cpp").resolve()),
                   "arguments": ["clang++", "-std=c++17", "-Igamma", "-fsyntax-only", "caller.cpp"]}
        (repo / "compile_commands.json").write_text(json.dumps([command]), encoding="utf-8")

The three directories are separate test repositories. Only the gamma class is visible to the caller; the alpha and beta headers represent unrelated alternatives in the indexed repository.

Inside each generated directory, this compiler check passed:

clang++ -std=c++17 -Igamma -fsyntax-only caller.cpp

For each directory, index its absolute path with index_repository, using a fresh graph cache (or an explicit full reindex). Use the returned project identifier with query_graph:

MATCH (f:Function)-[r:CALLS]->(c)
WHERE f.name = 'RunProbe'
RETURN c.name, c.file_path, r.strategy, r.confidence
LIMIT 20

Expected

All four calls in RunProbe target the methods in gamma/probe_device.h.

If this receiver cannot be resolved within the supported analysis model, an unresolved/ambiguous result would be preferable to attributing the calls to unrelated alternatives. This does not ask for a particular implementation.

Actual

Both quoted_indirect_forward and search_path_indirect_forward produce:

Method Target file Strategy Confidence
Open alpha/probe_device.h suffix_match 0.28
Close alpha/probe_device.h suffix_match 0.28
Read beta/probe_device.h suffix_match 0.28
Write beta/probe_device.h suffix_match 0.28

All four targets are wrong for this translation unit. The search_path_indirect_forward case includes a generated compile_commands.json with -Igamma.

The direct_control case targets gamma/probe_device.h for all four methods, with lsp_base_dispatch / 0.95. The control changes both the include form and the extra forward-declaration include, so it does not isolate either as the sole cause.

Partial-parse and unusable-parse counts were both zero in all three cases. These wrong edges are low-confidence heuristic edges; that limitation is visible, but they still identify unrelated methods as callees.

Related

No proprietary source, identifiers, absolute machine paths, project-derived IDs or internal logs are included.

Activity

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

    cypherCypher query language parser/executor bugsparsing/qualityGraph extraction bugs, false positives, missing edgeswindowsWindows-specific issues

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions