From 5a66a9f725150fcfb7e362aa4ab5b1e5133d1884 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Wed, 7 Oct 2026 05:08:51 -0500 Subject: [PATCH] fix(go): a defined type links the types it is defined from 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 --- CHANGELOG.md | 1 + __tests__/fixtures/kernel-parity/torture.go | 23 ++ __tests__/go-defined-type-refs.test.ts | 393 ++++++++++++++++++++ __tests__/go-type-alias.test.ts | 5 +- codegraph-kernel/src/go.rs | 33 +- src/extraction/languages/go.ts | 39 +- src/extraction/tree-sitter.ts | 5 +- 7 files changed, 467 insertions(+), 32 deletions(-) create mode 100644 __tests__/go-defined-type-refs.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 08226636c..6552665c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Go, a call through an interface now reaches a method that the implementing struct gets from a type it embeds: prometheus's `targetScraper` gets `Report` from the `*Target` it embeds, so a call through the `scraper` interface now reaches `Target.Report`, and etcd's `Hasher.MemberID` reaches `EtcdServer.MemberID` through `hasherAdapter`. Before, the call reached only structs that declare the method themselves, often just a test double, so `codegraph_explore`'s call path stopped at the interface. The method is picked the way Go picks it, from the nearest embedded type, and none is linked when two embedded types at the same depth both have it or it comes from an embedded interface. Re-index Go projects after upgrading. - In Go, a type defined over a slice, map, function or basic type now counts as implementing the interfaces its methods satisfy, just as a struct does: prometheus's `type staticDiscoverer []*targetgroup.Group` is a `Discoverer`, gin's `type formSource map[string][]string` a `setter`, etcd's `type Int64Comparable int64` a `Comparable`, and an adapter like `type HandlerFunc func(…)` a `Handler`. Before, only structs were considered, so these types were missing from an interface's implementations in `codegraph_explore`, impact and the type hierarchy, and a call through the interface, like `Discoverer.Run` or `setter.TrySet`, stopped there instead of reaching their methods. Re-index Go projects after upgrading. - In Go, a type alias like `type Event = mvccpb.Event`, alone or inside a `type ( … )` group, is now indexed and links to the type it names, so impact on that type reaches the code that uses the alias. Before, aliases were missing: a use written through the alias's package, like `clientv3.Event` or prometheus's `storage.AOptions{…}`, linked to nothing, and one inside the package, like `&PrometheusVersion{}` in prometheus's `web` package, linked to whichever type of that name the project had, there a test helper's struct. A method called on a value of an alias's type now reaches the aliased type's method rather than a namesake's, and a generic alias like `type Items[T any] = List[T]` links to `List`, so `xs.Len()` on one reaches `List.Len` instead of nothing. Re-index Go projects after upgrading. +- In Go, a type defined from other types, like etcd's `type WatchChan <-chan WatchResponse` or gin's `type HandlerFunc func(*Context)` and `type HandlersChain []HandlerFunc`, now links to the types it is built from, so impact on `WatchResponse` or gin's `Context` reaches these types and the code that uses them. Before, such a type linked to nothing. One written through another package, like etcd's `type PutResponse pb.PutResponse`, links to that package's type, and a recursive one like `type stateFn func(*Lexer) stateFn` doesn't link to itself. Re-index Go projects after upgrading. - In Angular templates, a property binding, an interpolation, a structural directive or a control-flow block that calls one of the component's own members, like `[name]="icon()"`, `{{ label() }}`, `*ngIf="isOpen()"` or `@if (loading()) {`, now links the component to that member, and so does reading a getter, like `[disabled]="!canSave"`, or handing a method to a child component, like `[displayWith]="displayFn"` or `trackBy: trackById`. Before, only event bindings such as `(click)="save()"` were read, so a signal, a `computed` value, a getter or a method used only from a template had no callers and looked unused. A pipe, a template variable, a call on another object such as `form.reset()`, and a plain field read like `[value]="title"` still link nothing. Re-index Angular projects after upgrading. - React Router routes kept in a table of their own are now indexed: the ASP.NET Core React template's `AppRoutes` array that `App.js` maps into ``, a `routes` array imported into `useRoutes(routes)` or `createBrowserRouter(routes)` or returned by a function as in `useRoutes(routes(isLoggedIn))`, route objects kept one per file and listed in `createBrowserRouter([MainRoutes, LoginRoutes])`, and routes written in place in `useRoutes([…])`. Before, route objects were read only from a file that itself creates a data router, so these apps had no routes, and their `navigate('/login')` calls and `` links led nowhere. Each route links to the page its `element` renders, past a guard like `` or ``, and through `lazy(() => import(…))` to the page it loads rather than a same-named page elsewhere in the repository. An `index: true` route is the page at its parent's address, and a parent route around others counts as their layout. A `{ path, element }` list that nothing hands to the router, such as a menu, still makes no routes. Re-index React projects after upgrading. - In Go, a call or type written through an import whose path ends in a version, or in something other than the package's name, is now known to go through that import: `yaml.Unmarshal(…)` after `import "go.yaml.in/yaml/v3"` or `"gopkg.in/yaml.v3"`, `sqlite3.Error` after `import "github.com/mattn/go-sqlite3"`, `klog.V(2)` after `import "k8s.io/klog/v2"`. Before, only the last part of the path named such an import (`v3`, `yaml.v3`, `go-sqlite3`), so the name was matched on its own and could link to any project function, method or type that shared it: kubernetes' `klog.V(…)` calls were linked to a logging wrapper's `V` method, and etcd's `semver.Version` parameters to an unrelated `Version` function. A name from another module now links to nothing, and one through a package of your own project, like `kit.New()` after `import "example.com/kit/v2"`, links to that package's symbol. A comment in an import block is also no longer taken for the name of the import after it. Re-index Go projects after upgrading. Thanks @danusha2345 for the comment fix. (#2374) diff --git a/__tests__/fixtures/kernel-parity/torture.go b/__tests__/fixtures/kernel-parity/torture.go index 9e1070456..6831c9272 100644 --- a/__tests__/fixtures/kernel-parity/torture.go +++ b/__tests__/fixtures/kernel-parity/torture.go @@ -67,6 +67,29 @@ type IfaceAlias = interface { type WordAlias = uint +// Defined types reference what they are defined from, but their own type +// parameters, predeclared types and their own name written bare. +type HandlerFunc func(*Widget, pkga.Item) error + +type HandlersChain []HandlerFunc + +type WatchChan <-chan *Stack[int] + +type ( + Lookup map[Dur][]*pkga.Widget + Grid [MAX_ITEMS]Widget + Wrapped (Widget) + Item pkga.Item + Nested map[string]struct { + w Widget + *Base + } +) + +type Tree[T any] []*Tree[T] + +type stateFn func(*Widget) stateFn + func useAlias(a *Alias) LocalAlias { return LocalAlias{} } diff --git a/__tests__/go-defined-type-refs.test.ts b/__tests__/go-defined-type-refs.test.ts new file mode 100644 index 000000000..8d655a3b4 --- /dev/null +++ b/__tests__/go-defined-type-refs.test.ts @@ -0,0 +1,393 @@ +/** + * 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 (`type A = B`) + * 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. + * + * A defined type now references each type its right-hand side names, as an + * alias does: on the type's name, where resolution reads a package qualifier + * back, without its own type parameters or Go's predeclared types. Written + * bare, its own name is the declaration itself (`type stateFn func(*Lexer) + * stateFn`), which a recursive type names without depending on anything: no + * reference, so neither a self-edge nor an unresolved row for the name. + * + * Resolution reads the reference as any type position: a bare name is its own + * package's type, `pb.PutResponse` the import's, and a name written through a + * package the index doesn't know links to nothing. A defined type is a new + * type, though: unlike an alias, it does not have its underlying type's + * methods, so a method called on one is not looked up there. + */ +import { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import CodeGraph from '../src/index'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel'; +import type { ExtractionResult } from '../src/types'; + +const KERNEL_PATH = path.join( + __dirname, + '..', + 'codegraph-kernel', + 'prebuilds', + `${process.platform}-${process.arch}`, + 'codegraph-kernel.node' +); +const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1'; + +/** Every right-hand side a defined type can have, beside the types it names. */ +const DEFINED_SOURCE = `package gin + +import ( + "context" + + "github.com/gin-gonic/gin/render" +) + +// HandlerFunc defines the handler used by gin middleware as return value. +type HandlerFunc func(*Context) + +// HandlersChain defines a HandlerFunc slice. +type HandlersChain []HandlerFunc + +type Context struct { + handlers HandlersChain +} + +type WatchChan <-chan WatchResponse + +type WatchResponse struct{} + +type ( + // Render names another package's type of its own name. + Render render.Render + Lookup map[Key]*WatchResponse + Grid [4][4]Cell + Pipeline func(ctx context.Context, in <-chan Cell) (Key, error) + Wrapped (Cell) + Named Cell + Tables map[string]struct { + cell Cell + *Key + } +) + +type Generic[T any] List[T] + +type Pair[K comparable, V any] map[K]List[V] + +type stateFn func(*Lexer) stateFn + +type Tree []*Tree + +type List[T any] struct { + items []T +} + +type Cell struct{} + +type Key string + +type Dur int + +type Mode uint8 + +type Lexer struct{} +`; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +/** ` ` of every node but the file and its imports, in source order. */ +function declarations(result: ExtractionResult): string[] { + return result.nodes.filter((n) => n.kind !== 'file' && n.kind !== 'import').map((n) => `${n.kind} ${n.name}`); +} + +/** ` ` for every `references` ref out of a type declaration, sorted. */ +function typeRefs(result: ExtractionResult): string[] { + const byId = new Map(result.nodes.map((n) => [n.id, n])); + return result.unresolvedReferences + .filter((r) => r.referenceKind === 'references' && ['type_alias', 'struct', 'interface'].includes(byId.get(r.fromNodeId)?.kind ?? '')) + .map((r) => `${byId.get(r.fromNodeId)!.name} ${r.referenceName}`) + .sort(); +} + +describe('a Go defined type references the types it is defined from', () => { + let savedEnv: Record = {}; + + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['go']); + }); + + beforeEach(() => { + savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); + resetKernelForTests(); + }); + + afterEach(() => { + for (const k of ENV_KEYS) { + if (savedEnv[k] === undefined) delete process.env[k]; + else process.env[k] = savedEnv[k]; + } + resetKernelForTests(); + }); + + function extract(backend: 'kernel' | 'wasm', file: string, source: string): ExtractionResult { + if (backend === 'wasm') { + process.env.CODEGRAPH_KERNEL = '0'; + return extractFromSource(file, source, 'go'); + } + delete process.env.CODEGRAPH_KERNEL; + process.env.CODEGRAPH_KERNEL_LANGS = 'all'; + const result = tryKernelExtract(file, source, 'go'); + expect(result, `kernel extraction of ${file}`).not.toBeNull(); + return result!; + } + + const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const); + + for (const crlf of [false, true]) { + const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s); + const label = crlf ? ' (CRLF)' : ''; + + it.each(backends)(`a defined type is still one type_alias node: %s${label}`, (backend) => { + const result = extract(backend, 'gin.go', eol(DEFINED_SOURCE)); + expect(declarations(result)).toEqual([ + 'type_alias HandlerFunc', + 'type_alias HandlersChain', + 'struct Context', + 'type_alias WatchChan', + 'struct WatchResponse', + 'type_alias Render', + 'type_alias Lookup', + 'type_alias Grid', + 'type_alias Pipeline', + 'type_alias Wrapped', + 'type_alias Named', + 'type_alias Tables', + 'type_alias Generic', + 'type_alias Pair', + 'type_alias stateFn', + 'type_alias Tree', + 'struct List', + 'struct Cell', + 'type_alias Key', + 'type_alias Dur', + 'type_alias Mode', + 'struct Lexer', + ]); + }); + + it.each(backends)(`a defined type references each type it names: %s${label}`, (backend) => { + const source = eol(DEFINED_SOURCE); + const result = extract(backend, 'gin.go', source); + // A qualified name keeps its package in the source (`render.Render`, + // `context.Context`). Not referenced: the predeclared types (string, + // int, uint8, error, comparable), the type parameters T, K and V, and a + // type's own name written bare (stateFn, Tree). + expect(typeRefs(result)).toEqual([ + 'Generic List', + 'Grid Cell', + 'HandlerFunc Context', + 'HandlersChain HandlerFunc', + 'Lookup Key', + 'Lookup WatchResponse', + 'Named Cell', + 'Pair List', + 'Pipeline Cell', + 'Pipeline Context', + 'Pipeline Key', + 'Render Render', + 'Tables Cell', + 'Tables Key', + 'WatchChan WatchResponse', + 'Wrapped Cell', + 'stateFn Lexer', + ]); + // Each sits on its name, where resolution reads the qualifier back. + const lines = source.split(/\r?\n/); + for (const r of result.unresolvedReferences.filter((u) => u.referenceKind === 'references')) { + expect(lines[r.line - 1]!.startsWith(r.referenceName, r.column), `${r.referenceName} at ${r.line}:${r.column}`).toBe(true); + } + const render = result.unresolvedReferences.find((r) => r.referenceName === 'Render')!; + expect(lines[render.line - 1]!.slice(0, render.column).trimStart()).toBe('Render render.'); + }); + } +}); + +/** The module of the indexed tests: etcd's and gin's shapes, with namesakes in a package that sorts first. */ +const MODULE: Record = { + 'go.mod': 'module go.etcd.io/etcd\n\ngo 1.24\n', + 'api/v3/etcdserverpb/rpc.pb.go': [ + 'package etcdserverpb', + '', + 'type PutResponse struct {', + '\tHeader *ResponseHeader', + '}', + '', + 'type ResponseHeader struct{}', + '', + 'func (m *PutResponse) GetHeader() *ResponseHeader { return m.Header }', + '', + ].join('\n'), + // A lookup by name meets these first. + 'alpha/names.go': [ + 'package alpha', + '', + 'type PutResponse struct{}', + '', + 'func (m *PutResponse) GetHeader() int { return 0 }', + '', + 'type WatchResponse struct{}', + '', + 'type Context struct{}', + '', + 'type Op struct{}', + '', + 'type watcher struct{}', + '', + ].join('\n'), + 'client/v3/watch.go': [ + 'package clientv3', + '', + 'import pb "go.etcd.io/etcd/api/v3/etcdserverpb"', + '', + 'type PutResponse pb.PutResponse', + '', + 'type WatchChan <-chan WatchResponse', + '', + 'type WatchResponse struct {', + '\tCanceled bool', + '}', + '', + 'type Op struct{}', + '', + 'type watcher struct{}', + '', + 'func (w *watcher) Watch() WatchChan { return nil }', + '', + 'func header(r *PutResponse) { r.GetHeader() }', + '', + 'type stateFn func(*watcher) stateFn', + '', + ].join('\n'), + // Package clientv3, known to the index by its path (`v3`, `client`) only. + 'client/v3/ordering/util.go': [ + 'package ordering', + '', + 'import "go.etcd.io/etcd/client/v3"', + '', + 'type OrderViolationFunc func(op clientv3.Op) error', + '', + ].join('\n'), + 'gin/gin.go': [ + 'package gin', + '', + 'type HandlerFunc func(*Context)', + '', + 'type HandlersChain []HandlerFunc', + '', + 'type Context struct {', + '\thandlers HandlersChain', + '}', + '', + 'type Engine struct{}', + '', + 'func (engine *Engine) Use(middleware ...HandlerFunc) {}', + '', + ].join('\n'), +}; + +describe.each([ + ['default', 'LF'], + ['wasm', 'LF'], + ['default', 'CRLF'], + ['wasm', 'CRLF'], +])('an indexed Go module links its defined types (%s, %s)', (backend, eol) => { + let root = ''; + let cg: CodeGraph | undefined; + let kernel: string | undefined; + + beforeAll(async () => { + kernel = process.env.CODEGRAPH_KERNEL; + if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0'; + else delete process.env.CODEGRAPH_KERNEL; + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-defined-')); + for (const [rel, content] of Object.entries(MODULE)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), eol === 'CRLF' ? content.replace(/\n/g, '\r\n') : content); + } + cg = await CodeGraph.init(root, { index: true }); + }, 60_000); + + afterAll(() => { + cg?.destroy(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL; + else process.env.CODEGRAPH_KERNEL = kernel; + }); + + const nodeNamed = (file: string, name: string) => { + const nodes = cg!.getNodesInFile(file).filter((n) => n.name === name && n.kind !== 'import'); + expect(nodes, `${name} in ${file}`).toHaveLength(1); + return nodes[0]!; + }; + + /** `kind file:qualifiedName` of each non-contains edge out of `name` in `file`, sorted. */ + const linksFrom = (file: string, name: string) => { + const links = cg!.getOutgoingEdgesFrom([nodeNamed(file, name).id]) + .filter((e) => e.kind !== 'contains') + .map((e) => ({ kind: e.kind, target: cg!.getNode(e.target)! })) + .map(({ kind, target }) => `${kind} ${target.filePath}:${target.qualifiedName}`); + return [...new Set(links)].sort(); + }; + + /** `file:qualifiedName` of every node impact on `name` in `file` reaches, sorted. */ + const impactOf = (file: string, name: string) => + [...cg!.getImpactRadius(nodeNamed(file, name).id, 3).nodes.values()].map((n) => `${n.filePath}:${n.qualifiedName}`).sort(); + + it('a defined type references the types it names, each in its own package', () => { + expect(linksFrom('client/v3/watch.go', 'WatchChan')).toEqual(['references client/v3/watch.go:WatchResponse']); + expect(linksFrom('gin/gin.go', 'HandlerFunc')).toEqual(['references gin/gin.go:Context']); + expect(linksFrom('gin/gin.go', 'HandlersChain')).toEqual(['references gin/gin.go:HandlerFunc']); + }); + + it('a name written through an import is that package’s type, not the declaration of its name', () => { + expect(linksFrom('client/v3/watch.go', 'PutResponse')).toEqual(['references api/v3/etcdserverpb/rpc.pb.go:PutResponse']); + }); + + it('a recursive defined type has no edge to itself and no unresolved row for its name', () => { + expect(linksFrom('client/v3/watch.go', 'stateFn')).toEqual(['references client/v3/watch.go:watcher']); + expect(cg!.getUnresolvedReferencesFrom(nodeNamed('client/v3/watch.go', 'stateFn').id)).toEqual([]); + }); + + it('a name written through a package the index does not know links to nothing', () => { + expect(linksFrom('client/v3/ordering/util.go', 'OrderViolationFunc')).toEqual([]); + const refs = cg!.getUnresolvedReferencesFrom(nodeNamed('client/v3/ordering/util.go', 'OrderViolationFunc').id); + expect(refs.map((r) => `${r.referenceKind} ${r.referenceName}`)).toEqual(['references Op']); + }); + + it('a defined type does not have its underlying type’s methods', () => { + // `r.GetHeader()` on a PutResponse defined from pb.PutResponse: Go has no + // such method, so neither pb's nor alpha's is the callee. + expect(linksFrom('client/v3/watch.go', 'header')).toEqual(['references client/v3/watch.go:PutResponse']); + }); + + it('impact on a type reaches the defined types built from it, and their users', () => { + expect(impactOf('client/v3/watch.go', 'WatchResponse')).toEqual([ + 'client/v3/watch.go:WatchChan', + 'client/v3/watch.go:WatchResponse', + 'client/v3/watch.go:watcher::Watch', + ]); + expect(impactOf('gin/gin.go', 'Context')).toEqual([ + 'gin/gin.go:Context', + 'gin/gin.go:Engine::Use', + 'gin/gin.go:HandlerFunc', + 'gin/gin.go:HandlersChain', + ]); + }); +}); diff --git a/__tests__/go-type-alias.test.ts b/__tests__/go-type-alias.test.ts index 38f7e4443..9c4a3fe70 100644 --- a/__tests__/go-type-alias.test.ts +++ b/__tests__/go-type-alias.test.ts @@ -198,8 +198,10 @@ describe('Go alias declarations are extracted', () => { const source = eol(ALIAS_SOURCE); const result = extract(backend, 'client/v3/watch.go', source); // The package of mvccpb.Event / context.Context stays in the source; - // predeclared types (string, uint, error) are no references. + // predeclared types (string, uint, error) are no references. The + // defined types (WatchChan, Defined) reference theirs too. expect(typeRefs(result)).toEqual([ + 'Defined WatchResponse', 'Event Event', 'Handler Context', 'Handler WatchResponse', @@ -209,6 +211,7 @@ describe('Go alias declarations are extracted', () => { 'Plain WatchResponse', 'Ptr WatchResponse', 'Slice WatchResponse', + 'WatchChan WatchResponse', ]); expectRefsOnTheirNames(result, source); }); diff --git a/codegraph-kernel/src/go.rs b/codegraph-kernel/src/go.rs index f0ccbab76..f9584176b 100644 --- a/codegraph-kernel/src/go.rs +++ b/codegraph-kernel/src/go.rs @@ -590,8 +590,8 @@ impl<'t> Walker<'t> { Extra { docstring, is_exported, ..Extra::default() }, ); // (go has no `value` field — no TS-style type-ref walk or member - // extraction.) An alias references what its `type` field names; a - // defined type (`type_spec`) declares a type of its own. + // extraction.) An alias and a defined type (`type_spec`) alike + // reference what their `type` field names. if let Some(row) = row { let references = edge_kind_index("references").unwrap(); for ty in self.alias_type_names(node) { @@ -873,16 +873,15 @@ impl<'t> Walker<'t> { } /// goAliasTypeNames (languages/go.ts): the name nodes of the types an - /// alias's `type` field names, in source order, but its own type - /// parameters and the predeclared types. Only a `type_alias` is one here: - /// a generic alias, which tree-sitter-go 0.23 parses as a `type_spec` + /// alias's or a defined type's `type` field names, in source order, but + /// its own type parameters, the predeclared types and its own name written + /// bare (a recursive type's, `type stateFn func(*Lexer) stateFn`). A + /// generic alias, which tree-sitter-go 0.23 parses as a `type_spec` /// around an error, never reaches the kernel (its file defers to wasm). fn alias_type_names(&self, node: Node<'t>) -> Vec> { let mut names = Vec::new(); - if node.kind() != "type_alias" { - return names; - } let Some(ty) = node.child_by_field_name("type") else { return names }; + let own = node.child_by_field_name("name").map(|n| self.text(n)); let mut params: HashSet<&str> = HashSet::new(); if let Some(list) = node.child_by_field_name("type_parameters") { for decl in (0..list.named_child_count()).filter_map(|i| list.named_child(i)) { @@ -893,22 +892,32 @@ impl<'t> Walker<'t> { } } } - self.collect_alias_type_names(ty, ¶ms, &mut names); + self.collect_alias_type_names(ty, ¶ms, own, false, &mut names); names } - fn collect_alias_type_names(&self, node: Node<'t>, params: &HashSet<&str>, out: &mut Vec>) { + /// `qualified`: the name of a `pkg.Name`, which is that package's + /// whatever this one declares. + fn collect_alias_type_names( + &self, + node: Node<'t>, + params: &HashSet<&str>, + own: Option<&str>, + qualified: bool, + out: &mut Vec>, + ) { stack_guard!(); if node.kind() == "type_identifier" { let text = self.text(node); - if !params.contains(text) && !is_go_predeclared_type(text) { + if !is_go_predeclared_type(text) && (qualified || (!params.contains(text) && own != Some(text))) { out.push(node); } return; } + let qualified = node.kind() == "qualified_type"; for i in 0..node.named_child_count() { if let Some(c) = node.named_child(i) { - self.collect_alias_type_names(c, params, out); + self.collect_alias_type_names(c, params, own, qualified, out); } } } diff --git a/src/extraction/languages/go.ts b/src/extraction/languages/go.ts index 1ecbf8612..c6957c55d 100644 --- a/src/extraction/languages/go.ts +++ b/src/extraction/languages/go.ts @@ -65,20 +65,23 @@ export function goEmbeddedTypeName(type: SyntaxNode | null | undefined, source: } /** - * The name nodes of the types a Go alias declaration names, in source order: - * `Event` in `type Event = mvccpb.Event`, `List` and `Event` in `type Page = - * List[Event]`, each where it is written, so resolution reads a package - * qualifier back. The alias's own type parameters (`T` in `type Items[T any] - * = List[T]`) and Go's predeclared types are not types it names. A generic - * alias has no rule in tree-sitter-go 0.23: it parses as a `type_spec` whose - * `=` is an error, and reads as an alias here too. Undefined for a defined - * type (`type WatchChan <-chan Event`), which declares a type of its own. + * The name nodes of the types a Go `type_alias` node names on its right-hand + * side, in source order: an alias's (`Event` in `type Event = mvccpb.Event`, + * `List` and `Event` in `type Page = List[Event]`) and a defined type's + * (`WatchResponse` in `type WatchChan <-chan WatchResponse`, `Context` in + * `type HandlerFunc func(*Context)`), each where it is written, so resolution + * reads a package qualifier back. Not types it names: the declaration's own + * type parameters (`T` in `type Items[T any] = List[T]`), Go's predeclared + * types, and its own name written bare, which in a recursive type (`type + * stateFn func(*Lexer) stateFn`) is the declaration itself. A generic alias + * has no rule in tree-sitter-go 0.23: it parses as a `type_spec` whose `=` is + * an error, and reads as any other declaration here. */ -export function goAliasTypeNames(node: SyntaxNode, source: string): SyntaxNode[] | undefined { - const isAlias = node.type === 'type_alias' || - (node.type === 'type_spec' && node.children.some((c: SyntaxNode) => c.type === 'ERROR' && getNodeText(c, source).trim() === '=')); - const type = isAlias ? getChildByField(node, 'type') : null; - if (!type) return undefined; +export function goAliasTypeNames(node: SyntaxNode, source: string): SyntaxNode[] { + const type = getChildByField(node, 'type'); + if (!type) return []; + const nameNode = getChildByField(node, 'name'); + const own = nameNode ? getNodeText(nameNode, source) : undefined; const params = new Set(); for (const decl of getChildByField(node, 'type_parameters')?.namedChildren ?? []) { for (const c of decl.namedChildren) { @@ -86,15 +89,17 @@ export function goAliasTypeNames(node: SyntaxNode, source: string): SyntaxNode[] } } const names: SyntaxNode[] = []; - const walk = (n: SyntaxNode): void => { + // `qualified`: the name of a `pkg.Name`, which is that package's whatever + // this one declares. + const walk = (n: SyntaxNode, qualified: boolean): void => { if (n.type === 'type_identifier') { const text = getNodeText(n, source); - if (!params.has(text) && !GO_PREDECLARED_TYPES.has(text)) names.push(n); + if (!GO_PREDECLARED_TYPES.has(text) && (qualified || (!params.has(text) && text !== own))) names.push(n); return; } - for (const c of n.namedChildren) walk(c); + for (const c of n.namedChildren) walk(c, n.type === 'qualified_type'); }; - walk(type); + walk(type, false); return names; } diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 8aa00c32e..358b766b2 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -3873,8 +3873,9 @@ export class TreeSitterExtractor { this.extractTsTupleContractNames(value, typeAliasNode); } } else if (this.language === 'go') { - // Go's `type Event = mvccpb.Event` names its type in the `type` field. - for (const type of goAliasTypeNames(node, this.source) ?? []) { + // Go's `type Event = mvccpb.Event` names its type in the `type` field, + // and so does a defined type, `type WatchChan <-chan WatchResponse`. + for (const type of goAliasTypeNames(node, this.source)) { this.unresolvedReferences.push({ fromNodeId: typeAliasNode.id, referenceName: getNodeText(type, this.source),