Skip to content

Resolve C++ scope-qualified static calls when the method exists only as a qualified fallback node - #3613

Open
ayushcodes10 wants to merge 5 commits into
Graphify-Labs:v8from
ayushcodes10:fix-2348-cpp-qualified-static-calls
Open

ayushcodes10 wants to merge 5 commits into
Graphify-Labs:v8from
ayushcodes10:fix-2348-cpp-qualified-static-calls

Conversation

@ayushcodes10

Copy link
Copy Markdown
Contributor

Summary

Fixes #2348.

A scope-qualified static call Class::Method(...) was not linked with a calls edge when the callee method node existed only in qualified form (label Class::Method()). This happens systematically for Unreal Engine UCLASS()/GENERATED_BODY() classes.

Root cause

Traced this to the bundled tree-sitter-cpp grammar's own error recovery, not graphify's extraction logic. GENERATED_BODY() (an unknown macro invocation to the grammar) followed by public: and a pointer-returning method declaration produces an ambiguous parse: the grammar's recovery heuristic can successfully resync on an unambiguous token (e.g. int FindNearest(...), where int is a primitive_type keyword), but for a self-referential pointer return type (AHelper* FindNearest(...), where AHelper is just an identifier) the recovery mis-attributes the whole declaration as a bogus constructor-initializer chain instead — so the in-class member declaration is silently dropped, and the out-of-line .cpp definition has nothing to attach to. The extractor's existing behavior in that case is to fall back to a node labeled with the qualified name (AHelper::FindNearest()), attributed to its file rather than its class.

_resolve_cpp_member_calls's method_index only ever indexes defines/method edge targets, so it can never find that qualified-only node — the call resolves via Foo::bar()'s exactly-one-type-candidate path but then has no method to land on, and the edge is silently dropped.

Fix

A grammar-level fix (teaching the bundled tree-sitter-cpp to parse Unreal's reflection macros) is out of scope for a Python extraction layer. Instead, a second-chance lookup: when the primary method_index lookup for a Foo::bar() call comes back empty, check for a node whose label is exactly the qualified form ("Foo::bar()") — still guarded by the same exactly-one-candidate rule every other resolver in this module already applies, so two unrelated classes that happen to share both a class name and a method name (and both hit the same parse failure) still correctly yield no edge.

Test plan

  • New tests in tests/test_cpp_objc_cross_file_calls.py: the issue's own Unreal-style repro now resolves to an EXTRACTED calls edge; two unrelated same-named classes both hitting the qualified-only fallback correctly yield no edge (god-node guard).
  • Full suite: 5623 passed, 68 skipped, 2 deselected, no regressions.
  • python3 -m tools.skillgen --check: OK.

🤖 Generated with Claude Code

ayushcodes10 and others added 5 commits September 16, 2026 23:01
Prep for resolving a scoped call whose callee exists only as a
qualified labeled fallback node: builds a label to node id index
for every node whose label looks like a qualified method
(Class colon colon method with parens), restricted to nodes with a
real source file. Nothing reads it yet, so no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Only the Foo colon colon bar branch tags a fallback label, since it
is the only one with a class name in source to build one from. The
lookup only runs when the primary method index lookup already came
back empty, and only accepts an exactly one candidate match, the
same guard every other resolver in this module already applies.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The issue's own repro, an Unreal style UCLASS whose method
declaration never survives header parsing, must still get a calls
edge from a site that invokes it through the qualified name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two different classes that both fail header parsing and happen to
share a class name and a method name must not let a call resolve to
either one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Resolves a C++ Class::Method() call to a method that exists only as a qualified-labeled node contained by its file rather than attached to its class via a defines/method edge — the case where Unreal's UCLASS()/GENERATED_BODY() macros defeat the grammar's error recovery so the header declaration never parses as a member. When the normal method_index lookup misses, _resolve_cpp_member_calls retries by the exact Class::Method() label and takes the match only when exactly one candidate exists, staying dangling when two unrelated classes share the label.

No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2222 functions depend on the 474 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 647 callers, 45 callees
  • new: _rebuild_code() — 129 callers, 54 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 125 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: collect_files() — 17 callers, 6 callees
  • …and 30 more — each is listed as a finding

Verification — 2222 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2047 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

286 of 286 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety — the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/test_affected_cli.py — full-run-safety
  • tests/test_affected_member_seed.py — full-run-safety
  • tests/test_agents_platform.py — full-run-safety
  • tests/test_analyze.py — full-run-safety
  • tests/test_anthropic_custom_endpoint.py — full-run-safety
  • tests/test_antigravity_install.py — full-run-safety
  • tests/test_apm_fallback_version.py — full-run-safety
  • tests/test_architecture_doc.py — full-run-safety
  • tests/test_astro_extraction.py — impact, full-run-safety
  • tests/test_astro_import_ids.py — impact, full-run-safety
  • tests/test_atomic_canvas_export.py — full-run-safety
  • tests/test_atomic_version_stamp.py — full-run-safety
  • tests/test_atomic_writes.py — full-run-safety
  • tests/test_backend_env_isolation.py — full-run-safety
  • tests/test_backend_extras.py — full-run-safety
  • tests/test_benchmark.py — full-run-safety
  • tests/test_benchmark_raw_graph.py — full-run-safety
  • tests/test_build.py — impact, full-run-safety
  • tests/test_build_merge_dedup_scope.py — full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py — full-run-safety
  • tests/test_build_merge_shrink_guard.py — full-run-safety
  • tests/test_builtin_global_type_refs.py — impact, full-run-safety
  • tests/test_cache.py — full-run-safety
  • tests/test_callflow_html.py — full-run-safety
  • tests/test_cargo_introspect.py — full-run-safety
  • tests/test_carried_hyperedge_remap.py — full-run-safety
  • tests/test_case_sensitive_resolution.py — impact, full-run-safety
  • tests/test_charmap_encoding.py — full-run-safety
  • tests/test_chunking.py — full-run-safety
  • tests/test_cjs_module_extension.py — impact, full-run-safety
  • tests/test_claude_cli_backend.py — full-run-safety
  • tests/test_claude_md.py — full-run-safety
  • tests/test_cli_broken_pipe.py — full-run-safety
  • tests/test_cli_export.py — full-run-safety
  • tests/test_cli_help.py — full-run-safety
  • tests/test_cluster.py — full-run-safety
  • tests/test_codebuddy.py — full-run-safety
  • tests/test_community_hub_labels.py — full-run-safety
  • tests/test_community_labels_skill.py — full-run-safety
  • tests/test_confidence.py — full-run-safety
  • tests/test_corrupt_graph_json.py — full-run-safety
  • tests/test_cpp_nested_and_cli.py — impact, full-run-safety
  • tests/test_cpp_objc_cross_file_calls.py — impact, changed-test, full-run-safety
  • tests/test_cpp_preprocess.py — full-run-safety
  • tests/test_cross_extension_reexport_self_cycle.py — impact, full-run-safety
  • tests/test_cross_language_call_resolution.py — impact, full-run-safety
  • tests/test_cross_repo_external_call_guards.py — impact, full-run-safety
  • tests/test_cross_repo_member_calls.py — impact, full-run-safety
  • tests/test_cross_repo_shared_types.py — full-run-safety
  • tests/test_csharp_call_site_generic_args.py — impact, full-run-safety
  • … and 236 more

non-code file(s) changed (CHANGELOG.md) → running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (CHANGELOG.md) — a coverage gap or a missing link — running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_resolve\_cpp\_member\_calls.

The verifier did not have enough to check \_resolve\_cpp\_member\_calls, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: non-vacuity: domain too small (only 1 distinct inputs exercised, need 3) — 'no divergence' would be near-vacuous

· 38 more finding(s) on lines outside this diff (see the check run).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C++ static calls to UCLASS methods are not linked (calls edge dropped) -> affected under-reports

1 participant