Skip to content

fix(go): a call on a package-level variable or constant links through the type it is declared with - #2667

Merged
colbymchenry merged 1 commit into
mainfrom
claude/go-package-value-receivers
Oct 19, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/go-package-value-receivers

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

Follow-up to #2664. Nothing read the declaration of a Go variable or constant declared at the top level of a package. A method call through one was therefore matched by the method's name alone:

#2664's scope reader skips these declarations on purpose ("outside every block it is a package-level name"), and the pattern scan only reads the calling function.

Fix

The change is matchGoPackageValueCall in src/resolution/name-matcher.ts, Go only. It applies to a call x.m() where x is a plain name, the file imports no package by that name, and no local or parameter binds it where the call is. Then a variable or constant node of the same package decides. Same package means the same directory and the same package clause, because a package foo_test file shares the directory.

  • Its spec is read, in its own file:

    • a written type (var lg *zap.Logger, RoleEndpoint Role = "endpoints");
    • a composite literal's type (var r = &Registry{});
    • a conversion's type (var p = Role("pod"));
    • for a constant written alone in a const ( … ) group, the spec above it, which Go repeats (KindB after KindA Kind = iota).
  • A project type gives its own method (or one its embeddings promote) at 0.9, through the package that declares it, read with the declaring file's imports.

  • These link to nothing: a type from outside the project, a predeclared type, a type literal, or a value an outside package hands out (var buf = bytes.NewBuffer(nil), var re = regexp.MustCompile(…)).

  • Calls the reader leaves alone. These go on to the strategies below, as before:

    • a value a project function returns (var testParser = parser.NewParser(…)), which says no type;
    • an untyped constant;
    • declarations that differ between build-tag files;
    • a project type whose methods the resolver can't find (runtime.Codec, defined over an interface).

    Typing a value from a call's result is fix(go): type receivers from package-qualified parameters and call results; no guess for outside types #1954's subject.

Tests

__tests__/go-package-value-receivers.test.ts has 8 cases:

  • On main 90d6501: 7 fail and 1 passes (the control).
  • With this change: 8 of 8 pass.
  • Against a first version without the runtime.Codec fallback: only the control fails.

Full suite on a6cfb52 plus this change: 610 files and 7,461 tests passed, 0 failed.

A/B

The two arms are main 90d6501 and this change, with the same kernel binary. #2665 (TS/JS barrel imports on sync) landed after the A/B and touches no Go path. The branch is rebased onto it (a6cfb52), and the tests ran there. Each repo got a full init -y per arm, and I diffed natural-key edge sets. The clones are the same ones #2664 used.

repo edges main → fix links dropped new links (0.9) retargeted (to 0.9) same target, now 0.9
gin 7,777 → 7,777 0 0 0 4
prometheus 100,036 → 100,017 59 40 0 0
etcd 64,274 → 64,299 18 43 0 3
harbor 100,291 → 100,313 2 24 38 34
kubernetes 1,238,157 → 1,238,371 46 260 405 430
  • Controls are identical. fmt (C++), llmc (C/CUDA/Python) and typeorm (TypeScript) have identical node, edge and unresolved-ref sets.
  • Cost: none measurable. Kubernetes index wall time, interleaved: main 128 s and 131 s, this change 130 s and 118 s. Earlier single runs were 164 s and 105 s.

Triage

I checked every dropped link, grouped by receiver, and every retargeted owner pair against the receiver's declaration in the source.

Dropped (125). Each is a value of a type from outside the project that main had bound to a project method by name:

  • prometheus:
    • 45 gogo-protobuf var xxx_messageInfo_Sample proto.InternalMessageInfo, which reached project Marshal and Merge methods;
    • 12 sync.Pool variables;
    • var noReferenceReleases = promauto.NewCounter(…), which reached noopMetric::Inc;
    • a regexp.MustCompile result.
  • etcd:
    • var lg *zap.Logger (15), which reached zapRaftLogger::Panic/Debug;
    • var rootCmd = &cobra.Command{…} (3), which reached migrationPlan::Execute.
  • harbor: var handler http.Handler, which reached the registryctl blob handler.
  • kubernetes:
    • var realClock = &clock.RealClock{} (13), which reached another package's private realClock::Now;
    • cobra Cmd… commands (13), which reached ServerRunOptions::Flags;
    • sync.Once, sync.Pool and atomic values;
    • kube-openapi Fake schemas;
    • var decode cbor.DecMode;
    • time.Date(…);
    • cel-go's checker.CostEstimate{…};
    • a template.Must(…) result.

Retargeted (443).

  • Kubernetes:
    • 371 sites are its registry pattern. Each registry package declares var Strategy = fooStrategy{…} and var StatusStrategy = …. Main bound Strategy.PrepareForUpdate(…) in those packages' tests, through the qualified name, to podcertificaterequest's exported Strategy type. Now they reach each package's own strategy type.
    • The rest follow their declarations: var SchemeGroupVersion = schema.GroupVersion{…}, var PluginHandler = &RegistrationHandler{}, var SimpleNameGenerator NameGenerator = simpleNameGenerator{}, var featureGate featuregate.FeatureGate.
  • Harbor: var configManager *cfgPkg.CfgManager (24, was the lib/config Manager interface by name), var authHelper *Auth, var helper = &authproxy.Auth{}.

New links (367).

Same target, now 0.9 (471). Main's guess was right, and the declaration now confirms it.

Kept on purpose.

  • kubernetes' var UnstructuredJSONScheme runtime.Codec = unstructuredJSONScheme{} is declared as runtime.Codec, a type defined over an interface whose methods its node doesn't carry. A first version linked those 6 calls to nothing. Now a project type with no method found leaves the call to the strategies below, which link it as before. The test pins this (JSONCodec).
  • Values a project function returns (var testParser = parser.NewParser(…)) are untouched. They're fix(go): type receivers from package-qualified parameters and call results; no guess for outside types #1954's subject (open, call-result typing).

🤖 Generated with Claude Code

… the type it is declared with

Nothing read the declaration of a Go variable or constant declared at the
top level of a package (the scope reader skips them, and the pattern scan
reads only the calling function), so a method call through one was matched
by the method's name: etcd's `var lg *zap.Logger` made `lg.Panic(...)` a
project logger's `Panic`, prometheus's protobuf `xxx_messageInfo_*` values
reached project `Marshal` methods, and kubernetes' registry tests' calls on
each package's `var Strategy = fooStrategy{...}` reached another package's
exported `Strategy` type.

matchGoPackageValueCall reads a same-package `variable` or `constant` node's
spec when the call's receiver is a plain name that no local or parameter
binds and no import names: a written type, a composite literal's or a
conversion's type, or for a constant alone in a `const ( ... )` group the
spec above it. A project type gives its own method (0.9) through the
package that declares it; a type or value from outside the project, a
predeclared type or a type literal gives nothing. A value a project
function returns, an untyped constant, declarations that differ between
build-tag files, and a project type whose methods aren't found are left to
the strategies below. A `package foo_test` file in the directory is another
package.

A/B on main 90d6501, same kernel for both arms, full index per arm:
- gin: 4 guesses now typed at 0.9.
- prometheus: 59 dropped (gogo-protobuf `xxx_messageInfo_*`, `sync.Pool`
  values), 40 new links (`RoleEndpoint.String()` and its siblings).
- etcd: 18 dropped (`var lg *zap.Logger`, a cobra command), 43 new links
  (`var display printer = &simplePrinter{}`), 3 now typed.
- harbor: 2 dropped (`var handler http.Handler`), 24 new, 38 retargeted
  (`var configManager *cfgPkg.CfgManager`), 34 now typed.
- kubernetes: 46 dropped (outside-typed values), 260 new, 405 retargeted
  (each registry package's own strategy type), 430 now typed.
fmt, llmc and typeorm are identical. Every dropped link, grouped by
receiver, and every retargeted owner pair was checked against the
receiver's declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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