Resolve C++ scope-qualified static calls when the method exists only as a qualified fallback node - #3613
Resolve C++ scope-qualified static calls when the method exists only as a qualified fallback node#3613ayushcodes10 wants to merge 5 commits into
Conversation
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>
There was a problem hiding this comment.
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-safetytests/test_affected_member_seed.py— full-run-safetytests/test_agents_platform.py— full-run-safetytests/test_analyze.py— full-run-safetytests/test_anthropic_custom_endpoint.py— full-run-safetytests/test_antigravity_install.py— full-run-safetytests/test_apm_fallback_version.py— full-run-safetytests/test_architecture_doc.py— full-run-safetytests/test_astro_extraction.py— impact, full-run-safetytests/test_astro_import_ids.py— impact, full-run-safetytests/test_atomic_canvas_export.py— full-run-safetytests/test_atomic_version_stamp.py— full-run-safetytests/test_atomic_writes.py— full-run-safetytests/test_backend_env_isolation.py— full-run-safetytests/test_backend_extras.py— full-run-safetytests/test_benchmark.py— full-run-safetytests/test_benchmark_raw_graph.py— full-run-safetytests/test_build.py— impact, full-run-safetytests/test_build_merge_dedup_scope.py— full-run-safetytests/test_build_merge_hyperedges_and_prune.py— full-run-safetytests/test_build_merge_shrink_guard.py— full-run-safetytests/test_builtin_global_type_refs.py— impact, full-run-safetytests/test_cache.py— full-run-safetytests/test_callflow_html.py— full-run-safetytests/test_cargo_introspect.py— full-run-safetytests/test_carried_hyperedge_remap.py— full-run-safetytests/test_case_sensitive_resolution.py— impact, full-run-safetytests/test_charmap_encoding.py— full-run-safetytests/test_chunking.py— full-run-safetytests/test_cjs_module_extension.py— impact, full-run-safetytests/test_claude_cli_backend.py— full-run-safetytests/test_claude_md.py— full-run-safetytests/test_cli_broken_pipe.py— full-run-safetytests/test_cli_export.py— full-run-safetytests/test_cli_help.py— full-run-safetytests/test_cluster.py— full-run-safetytests/test_codebuddy.py— full-run-safetytests/test_community_hub_labels.py— full-run-safetytests/test_community_labels_skill.py— full-run-safetytests/test_confidence.py— full-run-safetytests/test_corrupt_graph_json.py— full-run-safetytests/test_cpp_nested_and_cli.py— impact, full-run-safetytests/test_cpp_objc_cross_file_calls.py— impact, changed-test, full-run-safetytests/test_cpp_preprocess.py— full-run-safetytests/test_cross_extension_reexport_self_cycle.py— impact, full-run-safetytests/test_cross_language_call_resolution.py— impact, full-run-safetytests/test_cross_repo_external_call_guards.py— impact, full-run-safetytests/test_cross_repo_member_calls.py— impact, full-run-safetytests/test_cross_repo_shared_types.py— full-run-safetytests/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).
Summary
Fixes #2348.
A scope-qualified static call
Class::Method(...)was not linked with acallsedge when the callee method node existed only in qualified form (labelClass::Method()). This happens systematically for Unreal EngineUCLASS()/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 bypublic: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(...), whereintis aprimitive_typekeyword), but for a self-referential pointer return type (AHelper* FindNearest(...), whereAHelperis just anidentifier) 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.cppdefinition 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'smethod_indexonly ever indexesdefines/methodedge targets, so it can never find that qualified-only node — the call resolves viaFoo::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_indexlookup for aFoo::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
tests/test_cpp_objc_cross_file_calls.py: the issue's own Unreal-style repro now resolves to an EXTRACTEDcallsedge; two unrelated same-named classes both hitting the qualified-only fallback correctly yield no edge (god-node guard).python3 -m tools.skillgen --check: OK.🤖 Generated with Claude Code