Skip to content

fix(go): a defined type links the types it is defined from - #2441

Merged
colbymchenry merged 6 commits into
mainfrom
claude/mystifying-liskov-a3877b
Oct 7, 2026
Merged

colbymchenry merged 6 commits into
mainfrom
claude/mystifying-liskov-a3877b

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

A Go defined type such as etcd's type WatchChan <-chan WatchResponse (client/v3/watch.go), gin's type HandlerFunc func(*Context) or type HandlersChain []HandlerFunc was a type_alias node with no references edge to the types it is defined from. Impact on WatchResponse or gin's Context missed those declarations and everything that uses them. Only = aliases (#2417) named their right-hand side.

Fix

TS extractor and Rust kernel, mirrored:

  • goAliasTypeNames (src/extraction/languages/go.ts) and alias_type_names (codegraph-kernel/src/go.rs) now walk the type field of every type_alias node: a defined type's (a non-struct, non-interface type_spec) as well as an alias's.
  • Each type name becomes a references ref on that name, where resolution reads the package qualifier back.
  • Still skipped: the declaration's own type parameters and Go's predeclared types. Newly skipped: the type's own name written bare (see the self-reference section below). A qualified name is never skipped: type PutResponse pb.PutResponse names pb's type.

No resolver change.

Design points

Method lookup stays alias-only. A defined type is a new type, and Go doesn't give it the methods of the type it is defined from. goAliasTarget still recognizes only declarations with =, so resolveGoMethodInPackage doesn't follow a defined type's new references edge. A test pins it: r.GetHeader() on type PutResponse pb.PutResponse stays unresolved, rather than reaching pb's GetHeader or a namesake's.

The unknown-qualifier gate covers defined types too, and that's wanted. #2417's gateTargetKind rule leaves a references ref from any type_alias unresolved when it is written through a package the index doesn't know.

  • Measured on real code it changes nothing. After fix(go): an unaliased import is also known by the name goimports assumes for its package #2410, no defined type in 12 repos is written through such a qualifier: gin, etcd, prometheus, kubernetes, harbor, caddy, go-ethereum, grpc-go, hugo, kapacitor, gin-vue-admin and echo-realworld (kubernetes alone has 1,076 type names written through a known project import). An arm with the rule disabled gave graphs identical to this branch on gin, etcd and prometheus.
  • Where it does fire, it prevents real errors. In a repro module with an unaliased import "go.etcd.io/etcd/client/v3" (package clientv3, which the index knows only as v3 or client), disabling the rule bound every such name to the same-file declaration of its name:
    • type Op clientv3.Op linked itself;
    • type WatchResponse clientv3.WatchResponse linked itself;
    • type Watcher clientv3.Op and func(op clientv3.Op) linked the local ordering.Op.
  • With the rule, all of these stay unresolved.

A recursive type's self-reference is skipped at extraction. Example: prometheus' type stateFn func(*Lexer) stateFn, the only one in the three repos.

Validation

Before/after full indexes on current main (ed199e6, which has #2417, #2414, #2416 and #2419), with site-keyed diffs. Each added edge was judged against Go's scoping: a bare name is its own package's type, and pkg.X is the type of the package whose real package clause is pkg.

repo edges added bare → own package pkg.X → that package dropped / retargeted / metadata-only
gin +13 13 ok — 0 / 0 / 0
etcd +109 50 ok 59 ok 0 / 0 / 0
prometheus +114 79 ok 35 ok 0 / 0 / 0
kubernetes +1,601 521 ok 1,080 ok 0 / 0 / 0
  • Every added edge is a references edge from a defined type to a struct, interface or other type, in the right package. There are 0 new self-loops, and no node, synthesized-edge or metadata changes.
  • Every ref left unresolved goes through a package outside the project:
    • gin 1, etcd 22, prometheus 38;
    • kubernetes 262, e.g. context, http, time, k8s.io/utils/cpuset, k8s.io/gengo/v2/types.
  • Kernel and wasm indexes are identical on gin, etcd and prometheus.
  • scripts/kernel-parity.mjs --lang go: gin 99/99, prometheus 736/736, etcd 1065/1105 with 0 diffs. etcd's 40 deferred files are the same 40 as before.

Impact at depth 3, before → after:

symbol nodes files newly reached
etcd WatchResponse 147 → 178 55 → 66 WatchChan and its users
gin Context 664 → 725 37 → 40 HandlerFunc, HandlersChain, RecoveryFunc, Skipper
etcd pb PutResponse 80 → 146 38 → 63 clientv3's PutResponse and its users

Tests

  • New __tests__/go-defined-type-refs.test.ts:
    • extraction in both backends, LF and CRLF. It covers every right-hand-side shape (function, slice, map, channel, array, parenthesized, qualified, generic, an anonymous struct inside) and the skipped names (type parameters, predeclared types, a bare self-name), with each ref sitting on its name;
    • an indexed module under kernel and wasm, LF and CRLF. A namesake package sorts first. The tests cover own-package and import resolution, the recursive type (no self-edge, no unresolved row), an unknown qualifier (no link), a defined type not getting its underlying type's methods, and impact.
    • Of its 32 tests, 24 fail before this change. The 8 that pass before are the node-kind and no-methods guards.
  • __tests__/go-type-alias.test.ts: the defined types in fix(go): a type alias type A = B is indexed and links to the type it names #2417's fixture (WatchChan, Defined) now reference too.
  • __tests__/fixtures/kernel-parity/torture.go gains the defined-type shapes, so parity is checked in LF and CRLF.
  • All 48 test files with a Go fixture, plus dead-code, pass: 1,469 tests. go-nested-module.test.ts skipped one describe under load in the batch run and passed 11/11 when rerun alone.
  • Full suite (6,698 tests) on a box at 100% CPU:
  • Incremental sync links a defined type's reference once the type it names is added (checked with the CLI: HandlerFunc → a later context.go's Context).

Notes

  • Built on fix(go): a type alias type A = B is indexed and links to the type it names #2417, which landed while this was in progress; this branch is rebased onto it.
  • Go struct fields reference nothing either: there are no references edges out of structs at all, so impact on ResponseHeader misses PutResponse{Header *ResponseHeader}. That gap is filed as a follow-up task rather than widened into this PR.

🤖 Generated with Claude Code

colbymchenry and others added 6 commits October 7, 2026 07:57
A Go defined type (`type WatchChan <-chan WatchResponse`, gin's
`type HandlerFunc func(*Context)` and `type HandlersChain []HandlerFunc`)
is a `type_alias` node, and it referenced nothing: only an `=` alias
(#2417) named the types on its right-hand side. Impact on
`WatchResponse` or gin's `Context` missed the declarations built from
them, and everything that uses those.

Fix (TS extractor and the Rust kernel, mirrored): goAliasTypeNames /
alias_type_names walk the `type` field of every type_alias node, a
defined type's as well as an alias's, so each type it names becomes a
`references` ref on that name, where resolution reads the package
qualifier back. Still skipped: the declaration's own type parameters,
Go's predeclared types, and now its own name written bare, which in a
recursive type (prometheus' `type stateFn func(*Lexer) stateFn`) is the
declaration itself: no self-edge, and no failed row that would make
dead code treat every namesake as referenced. A qualified name is never
skipped (`type PutResponse pb.PutResponse` names pb's).

Resolution is unchanged. A defined type is a new type: goAliasTarget
still follows `=` aliases only, so a method called on a defined type
is not looked up on its underlying type. The gateTargetKind rule that
leaves a type_alias's reference written through a package the index
doesn't know unresolved now covers defined types too; without it, every
such name bound to the same-file declaration of its name (`type Op
clientv3.Op` linked itself).

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