Skip to content

fix(go): a type alias type A = B is indexed and links to the type it names - #2417

Merged
colbymchenry merged 10 commits into
mainfrom
claude/gifted-bhaskara-0b67b7
Oct 7, 2026
Merged

colbymchenry merged 10 commits into
mainfrom
claude/gifted-bhaskara-0b67b7

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 7, 2026 •

Copy link
Copy Markdown
Owner

Problem

A Go alias declaration (type Event = mvccpb.Event, type ( A = B )) was in neither extractor's graph. tree-sitter-go parses it as a type_alias node beside the type_spec of a defined type, and both the wasm extractor (languages/go.ts listed only typeAliasTypes: ['type_spec']) and the kernel (go.rs matched only type_spec) read type_spec alone. So etcd's client/v3/watch.go:52 type Event = mvccpb.Event had no node while line 54's type WatchChan <-chan WatchResponse did.

What that did downstream, measured on main (fe95183, and again on 31c3328):

  • A use written through the alias's package (clientv3.Event, prometheus's 390 storage.AOptions{…} literals) linked to nothing.
  • A use inside the package linked by name to whichever type of that name the project had: prometheus's &PrometheusVersion{} in web/ went to web/api/testhelpers's struct, and []Type in util/compression to tsdb/compression's Type.
  • A method called on an alias-typed value fell back to a lookup by name, which can pick another package's same-named method.
  • A generic alias (type Items[T any] = List[T]) has no rule in tree-sitter-go 0.23. It parses as a type_spec around an ERROR for the =, so the kernel defers the file and wasm's error recovery already made a type_alias node. That node named nothing, and method resolution then stopped at it: xs.Len() on an *Items[int] resolved to nothing.

Fix

Extraction (TS and kernel, kept at parity)

  • Go's typeAliasTypes is ['type_spec', 'type_alias']. The kernel's walker routes type_alias the same way.
  • An alias of a struct or interface literal becomes a struct / interface node, exactly as a type_spec does.
  • Otherwise it is a type_alias node with a references ref for each type its right-hand side names. Each ref sits on the name, so resolution reads a package qualifier back (Go: cross-package calls are not resolved when go.mod is not at the project root #2322). The alias's own type parameters and Go's predeclared types are skipped.
  • goAliasTypeNames (go.ts) and alias_type_names (go.rs) also recognize the generic-alias error shape on the wasm side. The kernel never sees it, since that file defers.

Resolution

  • resolveGoMethodInPackage treats an alias as the type it names. A method called on an *Event is looked up on mvccpb.Event, through any chain of aliases and pointers (type Ptr = *Local, type Local = WatchResponse).
  • An alias whose target can't be placed in the project (type Ctx = context.Context, func(…), map[…]…) resolves exactly as before, by name. Defined types (type Dur int) are not followed.
  • gateTargetKind: an alias's target written through a package that is none of the file's imports as indexed (clientv3 under an unaliased go.etcd.io/etcd/client/v3, which the index knows as v3 or, since fix(go): an unaliased import is also known by the name goimports assumes for its package #2410, client) stays unresolved. This is the rule fix(go): an interface links the interfaces it embeds #2397 applies to embedded types. Without it, name matching linked the alias to itself or to a namesake.

Validation

Tests: __tests__/go-type-alias.test.ts (14 tests).

Full suite (before the merge, on fe95183 + this change): 26 of 6496 failed in the parallel run on this shared Windows box. All were daemon / MCP / sync / watcher / perf-threshold tests: hook timeouts, EBUSY on teardown, and two timing bounds (2341 ms vs 2000, 101.7 ms vs 100).

Kernel parity sweep (scripts/kernel-parity.mjs --lang go), main 31c3328 vs this branch:

repo files at parity deferred to wasm diffs
etcd 1065/1105 → 1065/1105 40 → 40 0 → 0
prometheus 736/736 → 736/736 0 → 0 0 → 0
gin 99/99 → 99/99 0 → 0 0 → 0

The wasm totals move by exactly the aliases: etcd +13 nodes / +26 refs, prometheus +9 / +8. (Before #2401 landed, the sweep showed its 6 / 202 raft.None / compression.None diffs, the same count in both arms.)

A/B on real repos (shallow clones: etcd 76d58e3, prometheus 39c878f, gin 43fe48e). Each repo was indexed with real builds of main 31c3328 and of this branch, on the kernel path and, for etcd and prometheus, the wasm path too. Kernel and wasm graph dumps are byte-identical in each arm, and the diffs are the same on both paths, and the same as the first A/B against fe95183. Re-run against each later main, the delta was identical edge for edge every time, with no dispatch or implements edge changed: d8a7f86 (after #2410), 855eca2 (after #2414's promoted-method dispatch), 9d07864 (after #2415) and d348d30 (after #2416). Only the baselines moved: at d348d30, etcd went 63105 → 63215 and prometheus 96813 → 97294. Against 2f2afea (after #2419), prometheus adds the 4 implements/dispatch edges described below (+485), and etcd and gin are unchanged. The table shows the 31c3328 run.

Edges are diffed site by site (source, line, column, ref name), and each target is judged by Go's scoping: a bare name belongs to its own package, pkg.X to the import's package.

repo aliases edges retargeted added dropped
etcd 13 63209 → 63319 2 97 + 13 contains 0
prometheus 9 96824 → 97305 10 467 + 9 contains + 5 go-method-contains 0
gin 0 7674 → 7674 0 0 0

Every retargeted and added edge was checked:

  • Retargeted (12): all moved from another package's namesake to the alias in the reference's own package:
    • etcd's &Event{ ×2 in client/v3/watch_test.go, from mvccpb's struct;
    • prometheus's &PrometheusVersion{} ×7 in web/web_test.go, from web/api/testhelpers's struct;
    • []Type ×3 in util/compression, from tsdb/compression's Type.
  • Added (564): every one lands in the package Go reads the name from, and none is a self-loop:
    • uses through the alias's package that resolved to nothing before (clientv3.Event, pb.Watch_WatchServer, storage.AOptions{…}, compression.Type);
    • uses inside the package (KeyPredicate, recCase, the test files' sample);
    • the aliases' own refs to their targets, e.g. Event → mvccpb.Event, and the grpc stream aliases → their request/response structs, while grpc.ServerStreamingClient stays unresolved;
    • one embedding, pb.KV_RangeStreamServer.
  • go-method-contains (5): prometheus's func (c *KumaSDConfig) Name() and four more are written on the alias KumaSDConfig = SDConfig. The existing synthesizer already accepts type_alias owners, the same path that gives a defined type its methods, so these five methods, orphaned before, now hang off the alias.
  • With fix(go): a defined type implements the interfaces its methods satisfy #2419 merged (defined types as implementers, which also walks type_alias), the alias that owns those methods implements what they satisfy, standing in for SDConfig. On prometheus that is 1 go-implements edge, KumaSDConfig → discovery.Config, whose methods are Name, NewDiscoverer and NewDiscovererMetrics, and 3 interface-impl edges, so a call through discovery.Config now reaches the Kuma config's methods. An alias without methods of its own, the rest of them, implements nothing. The goImplementsEdges doc comment said a true alias is not extracted, so it now says what the alias owns.
  • Metadata only (1): etcd's p.Config.RevisionOffsetRange.Rand() still calls Range::Rand, at 0.7 instead of 0.9, because a second Rand (the antithesis package's type Rand = mrand.Rand) now exists.
  • Dead code: the type_alias rows of the dead-code report are unchanged on etcd and prometheus, with exported, test and generated code included.

The same module also reproduces the issue on the built CLI. On main, isCreate's e.IsCreate() and newEvent's Event{} go to the namesake package, and p.Err() (through two aliases) and xs.Len() (generic alias) resolve to nothing. This branch gives the alias nodes, Event → mvccpb.Event, and the calls on mvccpb.Event, WatchResponse and List.

Overlap

#1954 (open, @danusha2345) also teaches resolveGoMethodInPackage about aliases, reading type Name = Target from the package's files when the package has no node of that name. Once this lands, aliases have nodes, so that branch no longer fires and the alias-following here covers it. #1954's other parts are untouched by this PR:

  • standard-library-named variables;
  • qualified parameter types;
  • call-result receivers;
  • no edge for outside types.

Here, an alias of an outside type still resolves by name, as on main, which leaves #1954's "outside types get no edge" rule to it.

Not in this PR

  • A defined type (type WatchChan <-chan WatchResponse, gin's type HandlerFunc func(*Context)) still references nothing: before this PR no Go type declaration had a references edge to the type it is defined from. Aliases now do. Doing the same for defined types is a separate, larger graph change.
  • A doc comment above an ungrouped Go type (// Foo … then type Foo struct{}) is dropped by both extractors; only types inside a type ( … ) group keep theirs. Aliases behave like other types here.
  • Methods written on an alias receiver (func (c *KumaSDConfig) Name() with KumaSDConfig = SDConfig) belong to SDConfig's method set in Go. The graph keeps them under the alias, which implements discovery.Config in SDConfig's place. An interface whose methods are split between the two (some written on SDConfig, some on the alias) is satisfied by neither node.

🤖 Generated with Claude Code

colbymchenry and others added 10 commits October 7, 2026 00:53
…t names

tree-sitter-go parses `type Event = mvccpb.Event` as a `type_alias`, beside
the `type_spec` of a defined type, and both extractors read `type_spec`
only. etcd's client/v3 `Event` had no node; a use through its package
(`clientv3.Event`, prometheus's `storage.AOptions{…}`) linked to nothing,
and a use inside it linked by name to any type so named, another package's
included.

- Extraction (TS and kernel): an alias is a `type_alias` node referencing
  each type its right-hand side names, at the name so resolution reads the
  package back; an alias of a struct or interface literal is a struct or
  interface. The alias's own type parameters and Go's predeclared types are
  skipped. A generic alias, which tree-sitter-go 0.23 parses as a
  `type_spec` around an error, reads as an alias on wasm; the kernel defers
  its file.
- A method called on an alias-typed value is the aliased type's: an alias
  of a project type is followed in its own package, through pointers and
  further aliases; one of an outside type resolves by name as before.
- An alias's target written through a package that is none of the file's
  imports as indexed stays unresolved, as an embedded type's does, so the
  alias never links to itself or a namesake.

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

# Conflicts:
#	src/resolution/index.ts
…satisfy

With #2419, goImplementsEdges offers every Go type_alias as an
implementer. A true alias now has a node, and owns the methods written
with it as the receiver (prometheus's func (c *KumaSDConfig) Name(),
KumaSDConfig = SDConfig), which Go gives the aliased type; so the alias
implements discovery.Config in SDConfig's place. The doc comment said a
true alias is not extracted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit ed199e6 into main Oct 7, 2026
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 7, 2026
…o guess for outside types

Three receiver shapes still got no type, and a receiver typed outside the
project still got a guess by method name:

- A variable named like a standard-library package (`ring`, `token`,
  `parser`). Every `ring.X()` was skipped as a call into that package,
  whether or not the file imports it. It is skipped now only when the file
  imports the package; otherwise the receiver is typed like any other, and
  one whose type is unknown still gets no edge.
- A parameter or receiver of a package-qualified type (`s *store.Store`).
- A receiver bound to a call: `r := newRing()`, `s, err :=
  store.NewStore()`, `if r := find(id); r != nil`, a call spread over
  several lines, a conversion (`list := model.BotList(bots)`). The type is
  the callee's first result as its signature spells it, read in the
  callee's file. Only when the call is the whole right-hand side, and not
  when it takes the receiver itself.

A receiver whose declared type comes from a package outside the project's
modules (`conn net.Conn`, `ctx context.Context`, `req *http.Request`, an
alias of one, the result of an outside function) gets no edge:
`ctx.Done()` went to the one project type declaring a `Done`. A project
function's result declared as an outside type by value
(`http.RoundTripper`) is left as it was, since that is usually an
interface a project type implements.

Aliases of project types are followed by colbymchenry#2417, which gave them nodes; the
alias reader this change carried is gone, and an alias of an outside type
is recognized from that node's declaration.

Resolver only: no extraction change, the receiver scan keeps its
per-line memo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 7, 2026
…method (#2444)

Both extractors record `srv.(KVServer).Range(ctx, in)` as a bare `Range`
call at the column where its receiver expression starts, as they record
any call through an expression. Resolution matched the name alone, so
every handler protoc-gen-go-grpc emits went to the `UnimplementedKVServer`
stub beside the `KVServer` interface (72 edges in etcd), and calls like
`c.Reader.(*pipe).Close()`, `p.(Pausable).Pause()` or
`v.(featuregate.MutableFeatureGate).Set(…)` linked to a namesake or to
nothing.

The call's chain is now read back from its column (operand, selectors,
assertions, calls and indexes, past strings, runes and comments, and on to
the next line after a trailing `.`). When the link of the call's name
follows an assertion, the call is a method of the asserted type, found
where Go finds it:
- a bare `T` or `*T` in the call's own package, or one it dot-imports;
- a `pkg.T` in the imported project package;
- its own method, the one its interface declares, or one promoted from a
  type it embeds, through the package-scoped lookup #2361 added; an alias
  is followed to the type it names (#2417).

A type from outside the project (`http.Flusher`), a predeclared one
(`error`), an alias of an outside type (`type Ctx = context.Context`, for
which the package-scoped lookup would fall back to matching by name) or a
type literal (`interface{ Flush() }`) links nothing. The check runs ahead
of the framework, import and name strategies, and ahead of the built-in
filter, which dropped a method named `close` or `copy` called through an
assertion. Two calls of one name in a chain share the column
(`b.(*Builder).Add(1).Add(2)`); both are taken for the one made through the
assertion, whose edge is there either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
colbymchenry added a commit that referenced this pull request Oct 7, 2026
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>
danusha2345 pushed a commit to danusha2345/codegraph that referenced this pull request Oct 7, 2026
…o guess for outside types

Three receiver shapes still got no type, and a receiver typed outside the
project still got a guess by method name:

- A variable named like a standard-library package (`ring`, `token`,
  `parser`). Every `ring.X()` was skipped as a call into that package,
  whether or not the file imports it. It is skipped now only when the file
  imports the package; otherwise the receiver is typed like any other, and
  one whose type is unknown still gets no edge.
- A parameter or receiver of a package-qualified type (`s *store.Store`).
- A receiver bound to a call: `r := newRing()`, `s, err :=
  store.NewStore()`, `if r := find(id); r != nil`, a call spread over
  several lines, a conversion (`list := model.BotList(bots)`). The type is
  the callee's first result as its signature spells it, read in the
  callee's file. Only when the call is the whole right-hand side, and not
  when it takes the receiver itself.

A receiver whose declared type comes from a package outside the project's
modules (`conn net.Conn`, `ctx context.Context`, `req *http.Request`, an
alias of one, the result of an outside function) gets no edge:
`ctx.Done()` went to the one project type declaring a `Done`. A project
function's result declared as an outside type by value
(`http.RoundTripper`) is left as it was, since that is usually an
interface a project type implements.

Aliases of project types are followed by colbymchenry#2417, which gave them nodes; the
alias reader this change carried is gone, and an alias of an outside type
is recognized from that node's declaration.

Resolver only: no extraction change, the receiver scan keeps its
per-line memo.

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