Skip to content

fix(go): a bare init never binds to a package init or an init method - #2862

Merged
DeusData merged 2 commits into
mainfrom
fix/go-bare-init-never-binds
Oct 31, 2026
Merged

DeusData merged 2 commits into
mainfrom
fix/go-bare-init-never-binds

Conversation

@DeusData

Copy link
Copy Markdown
Owner

Problem

Go gives the identifier init no binding in the package block: it declares init functions and nothing else, and the spec says init functions "cannot be referred to from anywhere in a program". A method or field named init is only reachable through a selector (w.init(), e.init). So a bare init in Go is always a local name: a closure (init := func(ctx context.Context) {...}; init(ctx)), a parameter (func sum(init func() ref.Val)), or the package init clause of a directory named init.

The resolver did not know this. Before #1915 the flat package-init QN made same_module bind a bare init to its own package's init function. #1915 gave every init its own QN (pkg.init#file.go:N), which correctly stops that, but the bare name now falls through to the short-name fallback and binds unrelated methods named init instead. On kubernetes (8c078bc3, full index) every one of these edges was false:

  • 24 CALLS via suffix_match: 22 e2e storage DefineTests closures, one test-local closure, and the sum parameter (23 onto pluginwatcher.Watcher.init / lazyObject.init, 1 onto the field quotaEvaluator.init)
  • 28 WRITES from init := func(...) onto those methods
  • 7 USAGE, three of them from the package init clause of test/e2e/framework/*/init/init.go
  • 1 TESTS edge derived from one of the false CALLS

The rule

cbm_go_suppress_bare_init_bind (src/pipeline/registry.c), Go only. A bare init (a call that is not a selector call, a reference that is not the member half of a selector):

  • never binds a Function: every Go Function named init is a package init, so it is refused for every registry strategy, including the pre-fix(extract): give each Go init() its own QN #1915 same_module shape;
  • never binds a Method or Field namesake that only the name found (the shared weak drop-list: suffix_match, unique_name, field_type_hint, fuzzy).

lsp_* answers are left alone, because the sequential resolver emits them before its registry guards run and refusing one in only the parallel resolver would split the two. Selector calls are untouched, so w.init() keeps resolving to Watcher.init.

It is wired the same way as the neighbouring guards, at all six seams, and mirrored across both resolvers: CALLS (pass_calls.c and pass_parallel.c, beside the Java data-member guard), USAGE and READS/WRITES (pass_usages.c and pass_parallel.c, beside the bare-field guard).

Why Go-only, and why it is safe

The rule follows from the language spec, not from a heuristic. In Go no correct edge can come from a bare init to a package init, a method or a field. In other languages init is an ordinary name: in Java, C#, C++, Kotlin or Ruby a bare init() inside a method calls the class's own method, and in C, Python or JS an init function is called by its bare name. So the rule is gated on the file language, in line with the per-language suppressor rule.

Measured on kubernetes (full index, same corpus, fresh cache per binary)

edges into nodes named init main 9faa3e5 this branch
CALLS (bare, suffix_match) 24 0
CALLS (selector, lsp_type_dispatch / lsp_interface_dispatch) 14 14
WRITES 28 0
USAGE (bare) 7 0
USAGE (member access e.init) 1 1
TESTS 1 0

A whole-graph edge diff (type, source QN, target QN) between the two indexes shows that only these 60 edges were removed. Nodes are identical (355,618) and no CALLS/USAGE/WRITES/READS edge was added anywhere. The only other movement is SEMANTICALLY_RELATED, with 52 edges removed and 48 added out of about 255. This is expected: that pass builds corpus-wide TF-IDF / random-indexing vectors from each function's CALLS neighbours, so removing 24 CALLS shifts a few pairs near the threshold. Both binaries produce identical graphs across two runs each, so this movement is caused by the change and is not run-to-run noise.

Tests

  • pipeline_go_bare_init_binds_nothing and pipeline_go_bare_init_binds_nothing_parallel (tests/test_pipeline.c). The fixture has a package init in the caller's package, a local closure init that is called, written and passed by value, a parameter named init, a package init directory and an unrelated Watcher.init method. The tests assert that no CALLS/USAGE/WRITES/READS/CALL_REFERENCE edge from a bare init reaches any node named init, and that the decoys exist so the zero is not vacuous. The control asserts that the selector calls w.init() from Watcher.Start and from Boot still resolve to the method. The parallel twin uses 52 filler files and CBM_WORKERS=4.
  • go_bare_init_never_binds_init_namesake (tests/test_registry.c) pins the predicate: every strategy for the Function, weak strategies only for the Method/Field, lsp_* kept, selectors kept, other names and other languages untouched.
  • Revert check: with the predicate disabled, both pipeline twins fail with the four kubernetes shapes (closure CALLS, parameter CALLS, closure WRITES, package init USAGE, all onto Watcher.init). With it enabled, they pass.
  • Suites: registry, go_lsp, parallel, pipeline green on macOS. clang-format (LLVM) and cppcheck 2.20 are clean on the changed files.

Refs #1915.

Go gives `init` no binding in the package block: init functions cannot
be referred to from anywhere in a program, and a method or field named
init needs a selector. A bare `init` is therefore always a local name -
a closure (`init := func(ctx) {...}; init(ctx)`), a parameter, or the
`package init` clause of a directory named init.

Before #1915 same_module bound such a name to the flat package init.
With per-file init QNs it now falls through to the short-name fallback
and binds unrelated methods named init. On kubernetes that minted 24
CALLS (22 e2e storage DefineTests closures among them), 28 WRITES,
7 USAGE and one derived TESTS edge, all false.

cbm_go_suppress_bare_init_bind refuses, for a bare Go `init`, any
registry binding onto a Function (every Go Function named init is a
package init) and a weak short-name binding onto a Method or Field.
lsp_* answers and selector calls (`w.init()`) are untouched. Wired at
the CALLS, USAGE and READS/WRITES seams of both resolvers.

Kubernetes, full index, main vs branch: only those 60 edges removed,
nodes identical, no resolver edge added; the 14 LSP-typed selector
CALLS into init methods are unchanged.

Tests: pipeline_go_bare_init_binds_nothing (+ parallel twin) are RED
with the predicate disabled (closure CALLS, parameter CALLS, closure
WRITES, package-clause USAGE onto Watcher.init) and GREEN with it; the
selector control keeps Watcher.Start/Boot -> Watcher.init.
go_bare_init_never_binds_init_namesake pins the predicate.

Refs #1915

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData merged commit df30463 into main Oct 31, 2026
28 checks passed
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.

1 participant