Skip to content

fix(go): a call of a standard method name like err.Error() links to its receiver's declared type, never to one it shares a word with - #2664

Merged
colbymchenry merged 1 commit into
mainfrom
claude/gracious-chaum-f828d6
Oct 19, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/gracious-chaum-f828d6

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Summary

A Go call of a standard method name (GO_STD_METHODS: Error, String, Err, Lock, Len, Header, Equal, …) on a receiver whose type wasn't worked out was guessed by a word the receiver shares with a type's name. That word is what every err shares with an …Err type, and every ctx with a valCtx. On main b4e1013:

  • kubernetes' err.Error() calls link to notRegisteredErr::Error, prometheus's to ParseErr::Error, gin's to a test's TestErr::Error, and etcd's to ErrKeepAliveHalted::Error;
  • etcd's leaderID.String() links to MoveLeaderRequest::String;
  • a receiver declared as a type from outside the project links at 0.9 to a project type that shares its name: etcd's var pendingMu sync.Mutex reaches its own concurrency.Mutex, and harbor's w http.ResponseWriter reaches its ResponseRecorder.

A receiver declared with a predeclared type (err error) fell through to the same guesses.

Fix

The change is in matchMethodCall (src/resolution/name-matcher.ts). It applies only to Go, and only to a standard method name called on a plain variable.

  • The declaration in scope decides. The receiver's declaration in scope, found by fix(go): a dotted call is written through its own receiver, and a local named like an import is that variable #2448's scope reader goLocalDecl, takes precedence over the unscoped pattern scan.
    • A declared project type gives that type's method, at 0.9, through the package that declares it.
    • These link to nothing:
      • a predeclared type (err error);
      • a type from outside the project (var mu sync.Mutex, tb testing.TB);
      • a type literal;
      • a value that an outside package hands out (ctx, cancel := context.WithTimeout(…), endpoint := &url.URL{…});
      • a declared type that lacks the method.
  • Otherwise the receiver must name the owner type itself. Case and a trailing number are ignored (id names an ID, urlsMap a URLsMap).
    • Strategy 3 applies this rule through receiverNamesOwner, in both its unique-candidate (0.7) and its scoring (0.65) branch.
    • Strategy 2 (the capitalized receiver) now takes the owner type's own method. Before, it took any method in the owner's file whose qualified name contained the type's name, so status reached RuntimeStatus::String beside a Status type.
  • Only a declaration the scope reader finds rules a call out. The pattern scan reads back for the name without block scope, so it can land on another declaration. In kubernetes, version, _ := parse(…) came out as the test table's version string. In prometheus, var ( query promql.Query ) is in a var block the scan doesn't read, so it came out as query string. Valid Go calls only Error() on a predeclared type, so a predeclared type read that way says nothing.

Two refactors keep behavior the same: isGoOutsideImport is extracted from isGoExternalQualified, and namesOwnerType/flatWord are extracted from sharesReceiverWord. Every other language's path is unchanged.

Tests

__tests__/go-std-method-receivers.test.ts has 16 cases:

  • On main b4e1013: 12 fail and 4 pass. The passing ones are the three controls and versionText, which main already links right.
  • With this fix: 16 of 16 pass.
  • Against the first version of this fix: the two table-test cases (runQueries, versionText) fail. That version trusted the pattern scan's predeclared type, and the A/B below caught it dropping 57 links of other method names, like kubernetes' version.Major() and kubeadm's dryRun.Client().

A/B

The two arms are main b4e1013 and this fix, with the same kernel binary. #2662 landed after the A/B. It only changes JS/TS calls whose method name has a $, which Go calls never take. The branch is rebased onto it (0d33925), and the tests above ran there. Each repo got a full init -y per arm, and I diffed natural-key edge sets. Clones: gin 43fe48e8, prometheus 39c878f0, etcd 76d58e32, harbor b323c0c8, kubernetes 4c05e667.

repo edges main → fix links dropped calls newly typed (0.9) retargeted same target, now 0.9 std-name guesses (0.65/0.7) main → fix
gin 7,780 → 7,777 17 14 0 0 17 → 0
prometheus 100,212 → 100,036 265 89 6 5 256 → 2
etcd 64,557 → 64,274 335 52 7 33 351 → 4
harbor 100,459 → 100,291 174 6 6 1 171 → 4
kubernetes 1,240,405 → 1,238,157 2,489 241 77 29 2,463 → 7
  • Only standard method names change. No edge of any other method name changed in any repo. Node sets are identical everywhere.

  • 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 on the same box:

    run main fix
    pair 1 138 s 147 s
    pair 2 129 s 132 s
    earlier, under another session's load 139 s, 136 s 137 s
    first A/B 106 s 106 s

    The differences are within run-to-run noise.

Triage

Dropped guesses. These are 0.65 and 0.7 links, plus Strategy 2's 0.8 ones. I judged a seeded random sample of 132 drops (kubernetes 40, prometheus 25, etcd 25, harbor 25, gin 17) against each receiver's declaration. 3 were right on main:

  • harbor's buffer.Header(), after buffer := lib.NewResponseBuffer(w);
  • harbor's jStatus.String() (twice), after jStatus := job.Status(…).

Each is a local typed by a project function's result or a conversion, which nothing types today (see Overlap). The other 129 were wrong:

  • err.Error() on plain error values;
  • values of outside types: kubernetes' c.String(0) on randfill.Continue reaching a project C, endpoint := &url.URL{…}, queue *buffer.Ring;
  • package-level variables: var quotaCmdLock sync.Mutex reaching StripedLockSet::Unlock, errBrokeResumable = errors.New(…) reaching ErrKeepAliveHalted::Error;
  • prometheus's expr.String() on a parser.Expr reaching StepInvariantExpr::String, and result.String() on a bytes.Buffer reaching promql's Result::String.

I also checked every Strategy 2 drop in prometheus (18) and the kubernetes ones by target (43). All were wrong.

Typed links dropped. etcd 23, harbor 7, kubernetes 8. Every one is a type from outside the project bound to a project namesake:

  • etcd's var nowMu sync.Mutex, mux := sync.RWMutex{} and var pendingMu sync.Mutex, which reached its concurrency.Mutex and recipes.RWMutex;
  • harbor's var openAPIErr openapi.Error (go-openapi), which reached harbor's own Error::Error;
  • harbor's w http.ResponseWriter, which reached its ResponseRecorder;
  • kubernetes' var s fieldpath.Set, var remoteIP net.IP (which reached a CEL IP) and var lookback time.Duration (which reached metav1.Duration).

Retargeted.

  • Typed through the declaration:
    • prometheus's iter.Err() → chunks.Iterator::Err (was StringIter::Err), and expr.String() → Node::String;
    • etcd's gRPC-gateway server.Lock(…) → LockServer::Lock (was lockServer::Lock);
    • harbor's resource.String() → types.Resource::String (was ResourceMetadata::String);
    • kubernetes' a.String() on func(a, b labels.Selector) → Selector::String (was ResourceName::String).
  • By Strategy 2's own owner:
    • kubernetes' requirement.String() → Requirement::String (was Requirements::String);
    • entry.String() → ipset.Entry::String.
  • Some sites were wrong before and are still wrong (metric.String() on prometheus client_model's Metric).
  • One loss. At 4 etcd sites, result := ValidateAndReturnVisualize(…) returns a RobustnessResult. The exact owner now picks Result::Error, which includes() had gotten right by file order.

Newly typed. Calls like these now link through their declared type:

  • gin's c.String(…) in func(c *gin.Context) handlers;
  • prometheus's ss.Err() → SeriesSet::Err and lbls.String() → Labels::String;
  • etcd's localID.String() on a localID types.ID parameter.

Among the 241 kubernetes ones, a sample of 20 checked out right.

Remaining standard-name guesses are receivers named after their own type: overlaps.String() → Overlaps::String, urlsMap.String() → URLsMap::String, severity.String() → Severity::String. One exception: in kubernetes, six ip.Equal() calls on values a function returns, probably net.IP, still reach CEL's IP::Equal because the variable is named ip. Typing a local from a call's result (#1954) would settle those.

Overlap

Follow-ups (separate PRs)

  • Package-level variables and constants as receivers. I'll do this next, in its own PR. Nothing reads a package-level declaration's type for a receiver today:

    • etcd's var lg *zap.Logger, then lg.Panic(…), links to zapRaftLogger::Panic;
    • prometheus's var xxx_messageInfo_Sample proto.InternalMessageInfo links to Sample::Marshal;
    • harbor's var c *cache links to a test suite's method, at 0.9.

    Calls through a receiver that is a var or const of the caller's own package: prometheus 154 linked and 129 unresolved, etcd 28 and 193, harbor 122 and 141.

  • Outside-typed receivers for every method name, and locals typed by a call's result. Open fix(go): type receivers from package-qualified parameters and call results; no guess for outside types #1954 covers both, so I'm not duplicating it here. Sampling main's non-standard-name guesses with each receiver's declaration, such receivers sit behind about 1 in 8 kubernetes guesses (logger klog.Logger, w := httptest.NewRecorder()) and 1 in 4 etcd ones (lg *zap.Logger, ctx context.Context, conn, err := grpc.NewClient(…)).

🤖 Generated with Claude Code

… its receiver's declared type, never to one it shares a word with

A Go call of a standard method name (GO_STD_METHODS: Error, String, Err,
Lock, Len, Header, ...) on a receiver whose type wasn't worked out was
guessed by a word the receiver shares with a type's name: kubernetes'
`err.Error()` linked to `notRegisteredErr::Error`, prometheus's to
`ParseErr::Error`, etcd's `leaderID.String()` to `MoveLeaderRequest`, and
a receiver declared as a type from outside the project to a project type
of its name (etcd's `var pendingMu sync.Mutex` to its concurrency `Mutex`).

In matchMethodCall, for a standard method name on a plain Go variable:
- The receiver's declaration in scope (goLocalDecl, the scope reader)
  decides, over the unscoped pattern scan: a project type's own method,
  through the package that declares it; nothing for a predeclared type, a
  type from outside the project, a type literal, a value an outside
  package hands out, or a declared type that lacks the method.
- Otherwise a guess needs the receiver to name the owner type itself,
  case and a trailing number aside (`id` an `ID`, `urlsMap` a `URLsMap`):
  in Strategy 3 through receiverNamesOwner, and in Strategy 2, which took
  any method of the owner's file whose name contained the type's
  (`status` reached `RuntimeStatus::String` beside a `Status` type).
- Only a declaration the scope reader finds rules a call out. The pattern
  scan reads back for the name without block scope and can find another
  declaration of it, like a test table's `version string` above
  `version, _ := parse(...)`; valid Go calls no method but `Error` on a
  predeclared type, so such a read says nothing.

A/B on main b4e1013, same kernel for both arms, full index per arm:
- gin: 17 guesses dropped; 14 calls newly typed (`c.String(...)` in
  `func(c *gin.Context)` handlers).
- prometheus: 265 dropped, 89 newly typed, 6 retargeted.
- etcd: 335 dropped (23 of them typed links of `sync.Mutex` and
  `sync.RWMutex` values to project namesakes), 52 newly typed, 7
  retargeted.
- harbor: 174 dropped, 6 newly typed, 6 retargeted.
- kubernetes: 2489 dropped, 241 newly typed, 77 retargeted.
No link of any other method name changed, and the fmt, llmc and typeorm
controls are identical. Of 132 dropped links sampled, 3 were right
(locals typed by a project call's result, which no reader types yet).

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