diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d37c030..65080320c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - 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 struct that embeds another is no longer treated as overriding the methods it redeclares. gin's `Engine` embeds `RouterGroup` and has a `Use` of its own, but calling `Use` on a `RouterGroup` only ever runs `RouterGroup.Use`; before, `codegraph_explore`'s call path and impact went on from it into `Engine.Use` and the code only the engine runs. The same held for every struct or defined type embedded in another that redeclares one of its methods, like etcd's `Mutex` inside `lockerMutex`. A call through an interface still reaches each type that implements it, the embedding struct included. 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),