Skip to content

fix(go): an unaliased import is also known by the name goimports assumes for its package - #2410

Merged
colbymchenry merged 1 commit into
mainfrom
claude/quizzical-liskov-f31337
Oct 7, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/quizzical-liskov-f31337

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Problem

extractGoImports names an unaliased Go import by its path's last element. For a versioned or go- path, that element is not the name the code uses:

import registered as the code writes
go.yaml.in/yaml/v3 v3 yaml.Unmarshal(…), yaml.Node
gopkg.in/yaml.v3 yaml.v3 yaml.X
github.com/mattn/go-sqlite3 go-sqlite3 sqlite3.Error
k8s.io/klog/v2 v2 klog.V(2)

So the qualifier matched none of the file's imports, and everything that reads one treated the name as unqualified:

The name then resolved by itself to whatever project symbol shared it. #2397 guarded supertypes only (isGoUnknownQualified); calls and type references were still open. On kubernetes, 1,561 klog.V(…) calls went to the V method of a logging wrapper in apiserver/pkg/storage/etcd3/logger.go. 1,431 klog.Logger types went to the Logger interfaces of two test servers.

What changed

src/resolution/import-resolver.ts, resolver only (extraction and the kernel are untouched):

  1. An unaliased import also takes the name goimports assumes for its path (ImportPathToAssumedName): the last element that isn't a major version (/vN → the element before), with a go- prefix trimmed, cut at the first character an identifier can't hold. The last-element name stays registered, since k8s.io/api/core/v1 really is package v1. This is what keeps harbor's v1.Artifact{…} edge, which made fix(go): a comment in an import declaration is not part of an import #2374 drop a /vN rule that replaced the name.
  2. An assumed name is skipped when the file binds that name otherwise:
    • Another import already holds it. Kubernetes has 27 such imports, as in a file importing both pkg/apis/abac/v0 (assumed abac) and pkg/apis/abac.
    • Two unaliased imports assume it, so they can't both be that package. This happens 0 times in the five repos below.
  3. Only the import section is read, without comments: the text before the first top-level func / type / var / const that isn't inside a block comment.
    • A comment's last word had become the next import's name. For example, "github.com/golang/protobuf/proto" //nolint:staticcheck // TODO: remove for a supported version made etcd's following protoreflect import version.
    • The scan also no longer reads an import "…" in a doc comment or a string literal as an import (a code generator's template, a test fixture). Overlap with fix(go): a comment in an import declaration is not part of an import #2374 is described below.
    • Checked against the import specs tree-sitter extracts into the index, on every Go file of the five repos below that the index holds imports for (119,328 specs): this reader has 0 missing or extra names; main had 3 / 2 / 25 / 17 / 287 (gin, prometheus, etcd, harbor, kubernetes).
  4. Dot imports are unchanged: inside a block one is listed under its last element, and a single-line one yields nothing. A dot import gets no assumed name.

The doc comments of isGoUnknownQualified and its gateTargetKind call no longer cite yaml.Node. That supertype is now rejected as an outside package's. The guard's remaining case is a package named neither way, like clientv3 under a bare go.etcd.io/etcd/client/v3.

Validation

Tests

Before / after (full index, kernel path, main@31c3328d vs this branch, same kernel; natural-key edge diff; vendor/ excluded by default)

repo Go files edges main → PR removed added
gin 43fe48e 99 7,674 → 7,674 0 0
prometheus 39c878f 736 96,824 → 96,817 7 0
etcd 76d58e3 1,105 63,209 → 63,098 111 0
harbor b323c0c 1,605 98,325 → 98,309 33 17
kubernetes 4c05e667 13,501 1,230,096 → 1,223,709 6,669 282

gin, prometheus, etcd and harbor give the same diff on fe95183, before #2401. The same A/B on the first base, 24501fa (before #2402 and #2408), changed the same sites. Some of the removed false bindings had pointed elsewhere there: the Go framework heuristic that #2408 narrowed had picked etcd's *raft.Config → cache's Config and harbor's redis.Client → the dockerhub adapter's Client. Every changed edge was triaged at its site. The script read the qualifier written there and the import it names under each arm's naming, and checked that a qualified target lives in that import's package directory (module path stripped), or nowhere in the project for an outside import.

  • gin: no change. Its versioned imports (validator/v10, go-toml/v2, go-json, …) had no false bindings.
  • prometheus, 7 removed, all through an outside import that now has its name:
    • mmap.Map(…) had been linked to Labels::Map;
    • kingpin.Application (×2) and govultr.Instance to discovery/eureka's structs;
    • kingpin.Value to promql/parser's Value;
    • compute.Service to discovery/kubernetes's Service;
    • scaleway's instance.Server to stackit's Server.
  • etcd, 111 removed:
    • 84 semver/v3 refs: *semver.Version had gone to client/v3's Version function (37 → 20 incoming edges), wal's Version interface (31 → 6) and Cluster::Version (17 → 1).
    • 18 raft/v3 refs: raft.ReadState{…} had gone to a test double's ReadState method, raft.Logger to server::Logger, *raft.Config to ServerHealth::Config.
    • 5 protoreflect / proto refs from the comment fix.
    • 4 jwt/v5 calls. jwt.Parse had gone to jwtOptions::Parse. The other 3 were correct: jwt.assign / jwt.info in server/auth/jwt_test.go are calls on a local jwt, err := newTokenProviderJWT(…) that shadows the import. The outside-package check rejects every candidate, methods included. That rule already applied to imports named by their last element. These are the only correct edges lost across the five repos (see limits).
  • harbor, 33 removed, 17 added:
    • 31 outside: digest.Digest → Suite::Digest (23), go-redis/v9's redis.Client → harbor's own Client structs in common/http and pkg/clients/core (5), jwt.Claims → harbor's own claims/v2.Claims (2), gock.Request → notification.Request (1).
    • The rest is the comment fix:
    • &v1.Artifact{…} in preheat_test.go, an unaliased pkg/scan/rest/v1 import, keeps its edge on both arms.
  • kubernetes, 6,669 removed, 282 added:
    • 6,582 removed through outside imports. klog/v2 4,574, ginkgo/v2 1,904, go-restful/v3 47, semver/v4 30, dbus/v5 14, diskv/v3 7, blackfriday/v2 3, 1 each from go-socks5, go-jose/v4 and cbor/v2. By how they had been resolved: 5,010 instance-method calls, 1,551 exact-match type refs, 21 other. Effects:
      • the etcd3 logger wrapper's Errorf drops from 389 incoming edges to 5;
      • the kube-apiserver test server's Logger interface drops from 1,310 to 17;
      • Framework::BeforeEach drops from 384 to 1 (ginkgo.BeforeEach).
        None was flagged as a local variable shadowing the import.
    • 75 moved into the import's own package. For example, logs.AddFlags had gone to kubectl's LogsOptions::AddFlags and now reaches component-base/logs' AddFlags, and mount.Interface had gone to fsquota's Interface and now reaches mount-utils' Interface.
    • 11 same edges, now import 0.9.
    • 175 added through newly named project imports, all landing in the import's package:
      • 146 through k8s.io/mount-utils, a staging module whose package is mount;
      • the rest through imports a comment had renamed: component-base/logs, pkg/volume, cmd/*/app, admission/admit, dynamic-resource-allocation/kubeletplugin and others.
    • 18 added field-chain calls (b.mounter.IsLikelyNotMountPoint), because a field declared mount.Interface now has a known package.
    • 1 extends edge and 3 synthesized from it: removeall_test.go's fakeMounter embeds mount.FakeMounter. The 3 are an override interface-impl from FakeMounter, one from mount.Interface, and fakeMounter implements mount.Interface through the promoted methods fix(go): satisfying an interface counts the methods embedding brings in #2402 counts.
    • 1 receiver-word guess removed (err.Error → notRegisteredErr::Error, wrong). It was removed for an incidental reason, noted below.
  • Residual audit: an edge whose site qualifier names an outside import, or names a project import whose directory doesn't hold the target, counted under each arm's own naming.
    • prometheus and etcd: the same before and after (37 and 53).
    • harbor and kubernetes: 15 edges through a newly named outside import survive. The new name reaches them, but two existing weaknesses keep the outside-package check from applying:
      • The qualifier reader loses the qualifier when the name is spelled again on the line. For a dotted ref it falls back to the line's only X.Name, and a second spelling makes it give up: harbor's &distribution.Descriptor{Digest: digest.Digest(dig)} (9), kubernetes' klog.Infof("Log using Infof, …") and klog.Errorf(…) in a logs example, and cmp.Diff on a line that prints "Diff:".
      • The Express framework resolver runs on Go references. It declares JavaScript and TypeScript but is consulted for every ref, and its same-file pick skips the visibility check: klog.Logger → the same file's TContext::Logger method (3). On main it produced 944 Go edges on kubernetes and 7 on etcd.
        Both are follow-ups, below.
  • Rule hits: assumed names registered in 8 / 93 / 105 / 54 / 2,175 import specs (gin, prometheus, etcd, harbor, kubernetes); 27 skipped as held, 0 as ambiguous.

Performance (measured on the first base)

  • Import extraction, one pass over every .go file in one process: prometheus 18 → 28 ms, kubernetes 213 → 546 ms.
  • Full index, CPU time of all processes and threads, main vs this change, interleaved:
    • etcd: median 44.3 → 44.4 s;
    • prometheus: median 61.1 → 59.8 s;
    • kubernetes: 577 → 574 s, wall 163 → 164 s.
      All within run-to-run noise.

Overlap

Limits and follow-ups (not in this PR)

  • A local variable shadowing an outside import. An example is etcd's jwt, err := … beside import "github.com/golang-jwt/jwt/v5". isGoExternalQualified rejects method candidates too, so jwt.assign(…) loses its edge (3 on etcd). The rule predates this change; this change only makes more qualifiers recognizable. A fix would check whether the qualifier is a parameter or local at the site before treating it as the package.
  • A wrong assumed name. k8s.io/api/core/v1 assumes core but is v1; goimports adds an explicit name in such cases, so it mostly shows in hand-written imports. It is registered unless another import holds core. No added or moved edge landed outside its import's package. For a project package, the declared package clause would give the real name. That would also make clientv3.KV under a bare go.etcd.io/etcd/client/v3 resolve, which still goes through isGoUnknownQualified.
  • For a dotted ref, the qualifier reader reads the line instead of the ref's own receiver. It takes the line's only X.Name, so it reads the wrong qualifier when another X.Name precedes (klog.Error(err.Error()) gives klog for err.Error; this fired once on kubernetes and removed a wrong guess). It reads none when the name is spelled twice (the 12 residual sites above). Taking the receiver from the ref name would fix both.
  • The Express framework resolver resolves Go references (944 edges on kubernetes, 7 on etcd, on main). Framework resolvers aren't filtered by their languages.
  • Pre-existing false hubs independent of import names:
    • kubernetes' logger.V(…) on logger := klog.FromContext(ctx) still goes to the etcd3 wrapper's V (2,292 edges, a receiver-name guess for an outside type);
    • on prometheus, composite literals config.URL{…} become instantiates edges to a Target::URL method (20).

🤖 Generated with Claude Code

…mes for its package

An unaliased Go import was named by its path's last element, so the
qualifier in yaml.Node (go.yaml.in/yaml/v3), klog.V (k8s.io/klog/v2) or
sqlite3.Error (github.com/mattn/go-sqlite3) matched none of the file's
imports. The reference then resolved by its bare name to whatever project
symbol shared it.

An unaliased import now also takes the name goimports'
ImportPathToAssumedName gives its path (the last element that isn't a
major version, without a go- prefix, cut at the first non-identifier
character), unless another import of the file is bound to that name or a
second import assumes it too. The last element stays registered:
k8s.io/api/core/v1 really is package v1.

The import scan now reads only the import section, without comments: the
last word of a comment had become the next import's alias, and imports
spelled in doc comments or string literals were read as real ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit d8a7f86 into main Oct 7, 2026
colbymchenry added a commit that referenced this pull request Oct 7, 2026
`semver.Version` reads as an outside package since #2410, so its links
to project methods are gone before this change; `config.URL{...}` still
went to `scrape.Target.URL`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 7, 2026
…hich #2410 keeps unknown

#2410 makes an unaliased go.yaml.in/yaml/v3 known as yaml, so the test's
yaml.Node alias no longer reached the rule. An unaliased
go.etcd.io/etcd/client/v3 is known as v3 or client, never clientv3; with
the rule disabled, this case fails.

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