Repository navigation
fix(go): credit calls in a package-level var initialiser to the file (#2360) - #4867
nothariharan wants to merge 1 commit into
Conversation
…raphify-Labs#2360) extract_go walks only function and method bodies for calls. A package-level `var` initialiser was recursed into and never registered as a body, so a call there, most often inside a `var provider = func(...) {...}` closure kept as a swappable hook, produced no edge and its callee lost the caller. A package-level var_declaration is now walked by the existing walk_calls, owned by the file node: the initialiser runs at package init and has no enclosing function. A `var` inside a function is unchanged. The AST cache schema goes to 30.
|
Thanks for the pull request, @nothariharan. A maintainer will review it soon. Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions. A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic. |
There was a problem hiding this comment.
Graphify reviewed this change.
No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.
Not checked: tests were not run; formal verification proved 0 of 1 changed function(s) (1 sampled, not proven).
Formal verification. PR-changed functions: 1/1 verified (0 proven, 1 may-equivalent, 0 distinguished) · 0 not verified.
Graphify review — findings
Walks calls inside Go package-level var initialisers, including those inside a var f = func() {...} closure, and credits them to the file node (#2360). Previously these calls were skipped, so edges from package-init code never appeared in the graph. Bumps _AST_CACHE_SCHEMA to 30 so cached Go extractions re-run and pick up the new edges.
No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 2861 functions depend on the 125 functions this change touches.
Health — this change adds coupling hotspots:
- worse:
extract()— 1055 callers, 62 callees
Verification — 2861 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: 1492 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
51 of 470 test file(s) selected (11%) via static blast radius.
tests/test_binary_media_words.py— impacttests/test_cache.py— impacttests/test_cache_long_paths.py— impacttests/test_cache_stale_import_target.py— impacttests/test_charmap_encoding.py— impacttests/test_chunking.py— impacttests/test_csharp_partial_classes.py— impacttests/test_definition_file_portability.py— impacttests/test_detect.py— impacttests/test_extract.py— impacttests/test_extract_cache_location.py— impacttests/test_extract_cli.py— impacttests/test_go_package_var_calls.py— impact, changed-testtests/test_go_type_alias.py— impacttests/test_grammar_cache_key.py— impacttests/test_ignore_file_encoding.py— impacttests/test_incremental_mtime_collision.py— impacttests/test_java_member_calls.py— impacttests/test_js_member_collisions.py— impacttests/test_language_override_lifecycle.py— impacttests/test_language_overrides.py— impacttests/test_languages.py— impacttests/test_llm_backends.py— impacttests/test_manifest_ingest.py— impacttests/test_msbuild_dir_evidence.py— impacttests/test_multilang.py— impacttests/test_objc_field_table_remap.py— impacttests/test_out_dir_evidence.py— impacttests/test_partial_cache.py— impacttests/test_pipeline.py— impacttests/test_python_inherited_method_calls.py— impacttests/test_python_injected_factory_calls.py— impacttests/test_python_property_reads.py— impacttests/test_python_pytest_fixture_receivers.py— impacttests/test_rust_self_member_calls.py— impacttests/test_rust_use_trees.py— impacttests/test_semantic_cache_basename_recovery.py— impacttests/test_semantic_cache_out_root.py— impacttests/test_skill_persisted_scan_options.py— impacttests/test_skill_semantic_all_cached.py— impacttests/test_skillgen.py— impacttests/test_stale_prune.py— impacttests/test_stat_index_husk.py— impacttests/test_stat_index_multi_root.py— impacttests/test_stat_index_portability.py— impacttests/test_update_no_change_gate.py— impacttests/test_vbnet_extractor.py— impacttests/test_watch.py— impacttests/test_watch_manifest_location.py— impacttests/test_word_count_cache.py— impact- … and 1 more
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
No difference found (not proven): No behavior difference found in extract\_go (not a proof).
The verifier ran both versions of extract\_go on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
· 1 more finding(s) on lines outside this diff (see the check run).
Go (#4867/#4868/#4866) and Python (#4773) extractor changes alter per-file AST output and are AST-cached, so one combined _AST_CACHE_SCHEMA bump 30->31 forces stale caches to re-extract. Install (#4770/#4771) and the skill reword (#4721) need no bump. Folded into the unreleased 0.9.99. Each PR omitted its own bump / carried a stale number; the single combined bump lands here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Landed in v0.9.99, now on PyPI (pip install graphifyy==0.9.99). A call in a package-level var initialiser (and its closures) is credited to the file node. Thanks for the fix, @nothariharan. |
What does this PR do?
Refs #2360. On v8 the Go extractor walks only function and method bodies for calls. A call made in a package-level
varinitialiser is never walked, so it produces no edge and the callee loses that caller. The most common form is a closure kept in a package variable as a swappable hook:The same happens for a plain initialiser call such as
var registry = newRegistry().Why #2360 is not resolved yet on v8
#2360 is about package-level
var/constdeclarations missing from the graph. On v8,extract_go'swalkdispatches onfunction_declaration,method_declaration,type_declarationandimport_declarationonly. Avar_declarationis recursed into and never registered as a body, sowalk_callsnever sees the calls in it. The two open PRs for the issue (#2364, #3098) add nodes for the declared names, but neither walks the initialiser for calls: #2364'svar_declarationbranch mints the nodes and returns. So the calls stay missing with either of them.On Grafana,
pkg/cmd/grafana-cli/commands/resource_db_migrate.go:22-23holds exactly this shape:Extracting
pkg/api,pkg/api/routing,pkg/cmd/grafana-cli/commandsandpkg/storage/unified/sql/db/dbimpl(74 non-test files):dbimpl.ProvideResourceDB(dbimpl.go:41) has 0 incomingcallsedges, andresource_db_migrate.gohas no edge at lines 22-23.callsedges, none removed. All 4 new edges are package-init calls that are in the source:resource_db_migrate.godbimpl.ProvideResourceDB(nil, cfg, nil)insidevar resourceDBProvider = func(...)commands.gorunPluginCommand(installCommand)invar pluginCommands = []*cli.Command{...}commands.gorunRunnerCommand(resetPasswordCommand)invar adminCommandscommands.gorunDbCommand(datamigrations.EncryptDatasourcePasswords)invar adminCommandsThe change
extractors/go.py: avar_declarationwhose parent is thesource_file(a package-level one) is registered as a body owned by the file node and is walked by the existingwalk_calls. That includes the bodies of closures in it, sincewalk_callsstops only at function and method declarations. The file is the owner because the initialiser runs at package init and has no enclosing function. That matches how other extractors handle module-level code: the JS scan of #3408, for example, attaches what a module-level closure defines to the file node. Avardeclared inside a function is not affected: it is still walked as part of that function.cache.py:_AST_CACHE_SCHEMA29 → 30, so cached Go extractions re-run.Type of change
Verification & Invariants
Invariant: every call site in a Go file is attributed to the innermost named owner that runs it: a function, a method, or the file for package-level code. No call is credited to a function that does not contain it.
test_a_var_declared_inside_a_function_stays_with_the_function: a localvar x = helper()keepsRun -> helperand adds no file-level edge.Resolution is unchanged: the new raw calls go through the same in-file label match and cross-file import-evidence pass as any other Go call.
Read the CONTRIBUTING.md guide.
Reproduced the issue and identified the invariant.
Made the smallest fix necessary.
Added a regression test (if bug fix) or isolated boundary test.
Kept the PR description synchronized with the final implementation.
Documented any limitations / unsupported cases explicitly.
Not covered (unchanged):
varitself is still not a node, so a call through it (resourceDBProvider(cfg)in a function) still links nothing. That is the node half of Go: package-level var/const declarations are not extracted as nodes (skews query vocabulary in mixed Go/TS repos) #2360 (fix(go): extract package-level var/const as nodes (#2360) #2364, Go: package-level constants and variables become nodes #3098). If var nodes land, the var node is a more precise owner for its initialiser's calls than the file, and this branch would be the place to switch it.constdeclarations are not walked: a constant initialiser cannot call a function.How was this tested?
Run on Windows 11, Python 3.12, in the environment
uv run --all-extrassyncs (.venv; the later runs call.venv/Scripts/python -m pytestdirectly becauseuv runrewritesuv.lockon Windows).Graphify-specific checklist
uv run python -m tools.skillgen --bless) when changing their source fragments.