diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c0e7116..a474c4953 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,6 +66,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In a repository that holds more than one Shopify theme, a theme's `{% render %}`, `{% include %}` and `{% section %}` tags and the sections its JSON templates list now link only to that theme's own snippets and sections, which is where Shopify looks for them. Before, a section or snippet the theme doesn't have was linked to another theme's file of the same name, and a theme kept in a subfolder could link to the root theme's copy instead of its own. A reference the theme can't satisfy now links to nothing. Projects with a single theme are indexed as before. Re-index projects with several Shopify themes after upgrading. - In C, C++ and Objective-C, a struct or class member declared as a pointer, reference, array or function, like `SharedState* shared;`, `jv elements[];` or `virtual Status Put(…) = 0;`, no longer makes the struct look like it inherits from the member's type. Before, `codegraph_explore` and impact listed those types as base classes, every struct holding a pointer to a type as one of its subclasses, and a method of that struct as an override of the type's method with the same name. Real base classes like `class Derived : public Base`, Objective-C superclasses and Go's embedded fields are unchanged. Re-index C, C++ and Objective-C projects after upgrading. - Framework support now switches on when an app's `package.json` sits three or more folders deep, as with an ASP.NET solution's Angular or React app in `src/Web/ClientApp/`, or a React app in a workspace folder like `web/ui/mantine-ui/`. Before, only the project root and the first two folder levels were read, so frameworks such as Angular, React Router, Vue Router, SvelteKit and TanStack Router could go undetected there: an Angular app's routes, its `routerLink` and `router.navigate(…)` navigation and its template's event bindings were missing, so a handler used only from a `(click)` binding had no callers, and a React app's `navigate('/login')` calls didn't reach their route. Re-index projects laid out this way after upgrading. +- In Go, an interface that embeds another, like gin's `IRouter` embedding `IRoutes`, now links to it, so `codegraph_explore` and impact show the embedded interface as its base. Before, every embedded interface was dropped. An embedded field written through another package of your project, like `*refresh.Discovery` or `storage.Appender`, or with type arguments, like `List[int]`, now links too. One from the standard library or another module, like `io.Closer` or `sync.Mutex`, links to nothing rather than to a project type that shares its name, and a type-set constraint like `~int | ~float64` is not an embedding. An embedded type written without a package now always links to its own package's type: before, etcd's `Client`, which embeds its own `Lease` interface, was linked to a `Lease` struct in another package. Re-index Go projects after upgrading. ## [1.6.2] - 2026-10-03 diff --git a/__tests__/fixtures/kernel-parity/torture.go b/__tests__/fixtures/kernel-parity/torture.go index f89b8d429..3aada94af 100644 --- a/__tests__/fixtures/kernel-parity/torture.go +++ b/__tests__/fixtures/kernel-parity/torture.go @@ -17,6 +17,10 @@ var handlerTable = map[string]func(int){ type Widget struct { *Base Queryable + pkga.Embedded + *pkga.Pointer `json:"-"` + Stack[int] + error name string } @@ -26,10 +30,19 @@ type Stack[T any] struct { type Core interface { Reader + pkga.Closer // qualified + Lister[int] + error Marshal(v any) ([]byte, error) Unmarshal(data []byte) error } +type Number interface { + ~int | ~float64 +} + +type Exact interface{ int64 } + type Dur int func NewRegistry() *Registry { diff --git a/__tests__/go-interface-embedding.test.ts b/__tests__/go-interface-embedding.test.ts new file mode 100644 index 000000000..fece4451e --- /dev/null +++ b/__tests__/go-interface-embedding.test.ts @@ -0,0 +1,425 @@ +/** + * Go interface embedding produced no supertype edge in either extractor: + * + * type IRouter interface { // gin-gonic/gin: IRouter never reached IRoutes + * IRoutes + * Group(string, ...HandlerFunc) *RouterGroup + * } + * + * extractInheritance looked for a `constraint_elem`, a node type that neither + * the vendored tree-sitter-go wasm nor the kernel's grammar crate has. Both + * parse an embedded interface as a `type_elem` holding one type: a + * `type_identifier` for `IRoutes`, a `qualified_type` for `io.Closer`. A + * `type_elem` is also how a type-set constraint is written (`~int | ~string`, + * `float32 | float64`, a lone `int64`), and a constraint is no supertype. + * + * An embedded type is now read the same way in an interface and in a struct: + * `T`, `*T`, `pkg.T` and `T[X]` all embed `T`, recorded at the name itself so + * resolution reads the package back from the source. `storage.LabelQuerier` + * reaches that package's interface; `io.Closer` and `sync.Mutex` stay + * unresolved rather than landing on a project type that shares the name. + * Go's predeclared types are never recorded: `int64` is a constraint's term, + * and `error`, `any` and `comparable` exist in no graph. Runs against the + * native kernel (when built) and the wasm extractor, which must agree. + * + * Resolution keeps an embedded type in its package. A bare name is its own + * package's type, not another package's struct of that name, which the Go + * framework heuristics preferred (promql/parser's `Node` interface, etcd's + * `Lease`). A package that is none of the file's imports as indexed (`yaml` + * under an unaliased `go.yaml.in/yaml/v3`) leaves the type unresolved. + */ +import { describe, it, expect, beforeAll, beforeEach, afterEach } 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 { buildTypeHierarchy } from '../src/graph/type-hierarchy'; +import type { ExtractionResult, Node } 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'; + +/** Embedded interfaces of every shape, beside constraints that embed nothing. */ +const INTERFACE_SOURCE = `package store + +import ( + "io" + + "example.com/app/storage" +) + +type Queryable interface{ Q() } + +type Querier interface { + Queryable + io.Closer + storage.LabelQuerier // a project package's interface + Select(sorted bool) int +} + +type Lister[T any] interface{ List() []T } + +type Pager interface { + Lister[int] + Page() int +} + +type Timeout interface { + error + Timeout() bool +} + +type Number interface { + ~int | ~float64 +} + +type Float interface{ float32 | float64 } + +type Exact interface{ int64 } + +type Text interface{ ~string } + +type Ordered interface { + Number | Float | ~string +} +`; + +/** gin-gonic/gin's router interfaces. */ +const GIN_SOURCE = `package gin + +type HandlerFunc func(*Context) + +type IRoutes interface { + Use(...HandlerFunc) IRoutes + Handle(string, string, ...HandlerFunc) IRoutes +} + +type IRouter interface { + IRoutes + Group(string, ...HandlerFunc) *RouterGroup +} +`; + +/** Struct embedding takes the same shapes, and keeps its named fields out. */ +const STRUCT_SOURCE = `package store + +import ( + "sync" + + "example.com/app/storage" +) + +type Store struct { + *Engine + Reader + sync.Mutex + *storage.Base + List[int] + error + name string + engine *Engine + items []storage.Base +} +`; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +/** ` ` for every extends/implements ref, sorted. */ +function supertypeRefs(result: ExtractionResult): string[] { + const byId = new Map(result.nodes.map((n) => [n.id, n])); + return result.unresolvedReferences + .filter((r) => r.referenceKind === 'extends' || r.referenceKind === 'implements') + .map((r) => `${byId.get(r.fromNodeId)?.name ?? r.fromNodeId} ${r.referenceKind} ${r.referenceName}`) + .sort(); +} + +describe('Go embedded types are supertypes', () => { + 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!; + } + + /** Every supertype ref sits on its name, where resolution reads a package qualifier back. */ + function expectRefsOnTheirNames(result: ExtractionResult, source: string): void { + const lines = source.split(/\r?\n/); + const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'extends'); + expect(refs.length).toBeGreaterThan(0); + for (const r of refs) { + expect(lines[r.line - 1]!.startsWith(r.referenceName, r.column), `${r.referenceName} at ${r.line}:${r.column}`).toBe(true); + } + } + + 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)(`an embedded interface extends; a constraint does not: %s${label}`, (backend) => { + const source = eol(INTERFACE_SOURCE); + const result = extract(backend, 'store/querier.go', source); + expect(result.nodes.filter((n) => n.kind === 'interface').map((n) => n.name).sort()).toEqual( + ['Exact', 'Float', 'Lister', 'Number', 'Ordered', 'Pager', 'Querier', 'Queryable', 'Text', 'Timeout'] + ); + expect(supertypeRefs(result)).toEqual([ + 'Pager extends Lister', + 'Querier extends Closer', + 'Querier extends LabelQuerier', + 'Querier extends Queryable', + ]); + expectRefsOnTheirNames(result, source); + // The interfaces' own methods are still its members, the embeddings are not. + const querier = result.nodes.find((n) => n.name === 'Querier')!; + const members = result.edges + .filter((e) => e.kind === 'contains' && e.source === querier.id) + .map((e) => result.nodes.find((n) => n.id === e.target)?.name); + expect(members).toEqual(['Select']); + }); + + it.each(backends)(`gin's IRouter extends IRoutes: %s${label}`, (backend) => { + const result = extract(backend, 'routergroup.go', eol(GIN_SOURCE)); + expect(supertypeRefs(result)).toEqual(['IRouter extends IRoutes']); + }); + + it.each(backends)(`an embedded struct field of any shape extends: %s${label}`, (backend) => { + const source = eol(STRUCT_SOURCE); + const result = extract(backend, 'store/store.go', source); + expect(supertypeRefs(result)).toEqual([ + 'Store extends Base', + 'Store extends Engine', + 'Store extends List', + 'Store extends Mutex', + 'Store extends Reader', + ]); + expectRefsOnTheirNames(result, source); + }); + } +}); + +describe('the type hierarchy of an indexed Go module', () => { + let root = ''; + let cg: CodeGraph | undefined; + + afterEach(() => { + cg?.destroy(); + cg = undefined; + if (root) fs.rmSync(root, { recursive: true, force: true }); + root = ''; + }); + + it('links embedded interfaces and structs to the package that declares them', async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-embedding-')); + const files: Record = { + 'go.mod': 'module example.com/app\n\ngo 1.22\n', + 'storage/storage.go': [ + 'package storage', + '', + 'type LabelQuerier interface {', + '\tLabelValues(name string) []string', + '}', + '', + 'type Base struct{}', + '', + 'func (b *Base) Close() error { return nil }', + '', + ].join('\n'), + // Named like io.Closer, io.Reader, sync.Mutex and yaml.Node: never what those name. + 'other/other.go': [ + 'package other', + '', + 'type Closer interface{ Close() error }', + '', + 'type Mutex struct{}', + '', + 'type Reader interface{ Read(p []byte) (int, error) }', + '', + 'type Node struct{}', + '', + ].join('\n'), + 'store/store.go': [ + 'package store', + '', + 'import (', + '\t"io"', + '\t"sync"', + '', + '\t"example.com/app/storage"', + '\t"go.yaml.in/yaml/v3"', + ')', + '', + '// The package is yaml, though the index knows its import as v3.', + 'type RuleGroupNode struct {', + '\tyaml.Node', + '\tName string', + '}', + '', + 'type Queryable interface{ Q() }', + '', + 'type Querier interface {', + '\tQueryable', + '\tio.Closer', + '\tstorage.LabelQuerier', + '\tSelect(sorted bool) int', + '}', + '', + 'type Number interface {', + '\t~int | ~float64', + '}', + '', + 'type Engine struct{}', + '', + 'type Reader interface{ Read() }', + '', + 'type List[T any] struct{ items []T }', + '', + 'type Store struct {', + '\t*Engine', + '\tReader', + '\tsync.Mutex', + '\t*storage.Base', + '\tList[int]', + '}', + '', + ].join('\n'), + // Named like the io.Reader it embeds. + 'wrap/wrap.go': [ + 'package wrap', + '', + 'import "io"', + '', + 'type Reader interface {', + '\tio.Reader', + '\tPeek() byte', + '}', + '', + ].join('\n'), + 'gin/routergroup.go': GIN_SOURCE, + // prometheus's promql/parser: its own `Node` interface, while other/ has a `Node` struct. + 'parser/ast.go': [ + 'package parser', + '', + 'type Node interface{ String() string }', + '', + 'type Expr interface {', + '\tNode', + '\tType() string', + '}', + '', + 'type exprWrapper struct {', + '\tNode', + '}', + '', + ].join('\n'), + }; + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + cg = await CodeGraph.init(root, { index: true }); + const graph = cg; + + const one = (name: string, file: string): Node => { + const found = graph.getNodesByName(name).filter((n) => n.filePath === file && n.kind !== 'import'); + expect(found, `${name} in ${file}`).toHaveLength(1); + return found[0]!; + }; + const where = (n: Node | null) => `${n?.name}@${n?.filePath}`; + + const sources = [ + one('Querier', 'store/store.go'), + one('Number', 'store/store.go'), + one('Store', 'store/store.go'), + one('RuleGroupNode', 'store/store.go'), + one('Reader', 'wrap/wrap.go'), + one('IRouter', 'gin/routergroup.go'), + one('Expr', 'parser/ast.go'), + one('exprWrapper', 'parser/ast.go'), + ]; + const supertypes = graph + .getOutgoingEdgesFrom(sources.map((n) => n.id), ['extends', 'implements']) + .map((e) => `${graph.getNode(e.source)?.name} ${e.kind} ${where(graph.getNode(e.target))}`) + .sort(); + // An embedded interface of a struct resolves as `implements`; one of an + // interface stays `extends`. io.Closer, sync.Mutex, io.Reader and yaml.Node + // are outside the project, so nothing named like them is linked. A bare + // name is its own package's type, whatever kind another package's is. + expect(supertypes).toEqual([ + 'Expr extends Node@parser/ast.go', + 'IRouter extends IRoutes@gin/routergroup.go', + 'Querier extends LabelQuerier@storage/storage.go', + 'Querier extends Queryable@store/store.go', + 'Store extends Base@storage/storage.go', + 'Store extends Engine@store/store.go', + 'Store extends List@store/store.go', + 'Store implements Reader@store/store.go', + 'exprWrapper implements Node@parser/ast.go', + ]); + + // The outside supertypes are kept as unresolved references, not dropped. + const unresolved = (n: Node) => + graph + .getUnresolvedReferencesFrom(n.id) + .filter((r) => r.referenceKind === 'extends') + .map((r) => r.referenceName) + .sort(); + expect(unresolved(one('Querier', 'store/store.go'))).toEqual(['Closer']); + expect(unresolved(one('Store', 'store/store.go'))).toEqual(['Mutex']); + expect(unresolved(one('RuleGroupNode', 'store/store.go'))).toEqual(['Node']); + expect(unresolved(one('Reader', 'wrap/wrap.go'))).toEqual(['Reader']); + + // What the viewer's Type hierarchy and codegraph_explore read. Declared + // subtypes only: Go's implicit-satisfaction edges are synthesized from + // method names (Base's Close() satisfies other.Closer) and drawn apart. + const subtypes = (n: Node) => + (buildTypeHierarchy(graph, n)?.descendants ?? []) + .filter((d) => !d.synthesized) + .map((d) => `${d.relation} ${where(d.node)}`); + expect(subtypes(one('IRoutes', 'gin/routergroup.go'))).toEqual(['extends IRouter@gin/routergroup.go']); + expect(subtypes(one('LabelQuerier', 'storage/storage.go'))).toEqual(['extends Querier@store/store.go']); + expect(subtypes(one('Base', 'storage/storage.go'))).toEqual(['extends Store@store/store.go']); + const declaredInto = graph + .getIncomingEdgesTo(['Closer', 'Mutex', 'Reader', 'Node'].map((name) => one(name, 'other/other.go').id), ['extends', 'implements']) + .filter((e) => e.provenance !== 'heuristic'); + expect(declaredInto).toEqual([]); + expect( + (buildTypeHierarchy(graph, one('IRouter', 'gin/routergroup.go'))?.ancestors ?? []).map((a) => `${a.relation} ${where(a.node)}`) + ).toEqual(['extends IRoutes@gin/routergroup.go']); + }, 60_000); +}); diff --git a/codegraph-kernel/src/go.rs b/codegraph-kernel/src/go.rs index 239c788a6..efc67971b 100644 --- a/codegraph-kernel/src/go.rs +++ b/codegraph-kernel/src/go.rs @@ -4,8 +4,8 @@ //! Go's shape quirks, mirrored exactly: methods are top-level with a receiver //! (qualifiedName override `Recv::name` + a contains edge to the FIRST //! earlier-in-file struct of that name), structs/interfaces arrive as -//! `type_spec` and classify via the inner type node (struct embedding → -//! extends; interface method_elems become method nodes), composite literals +//! `type_spec` and classify via the inner type node (struct and interface +//! embedding → extends; interface method_elems become method nodes), composite literals //! (`pkga.Widget{}`) keep their package qualifier as `instantiates` refs, //! top-level var/const specs walk their initializers ATTRIBUTED to the //! declared symbol (#693), 2-hop field chains (`t.conn.Exec`) keep the chain @@ -846,20 +846,42 @@ impl<'t> Walker<'t> { } } - /// extractInheritance — the Go branches: interface embedding - /// (constraint_elem) and struct embedding (field_declaration without a - /// field_identifier), plus the field_declaration_list recursion. + /// goEmbeddedTypeName (languages/go.ts): the name node of the type an + /// embedding names — `T`, `*T` (the `*` is a sibling token), `pkg.T` and + /// `T[X]` all embed `T`. A qualified type yields its name; resolution reads + /// the package back from the source at that position. None for any other + /// type and for a predeclared one. + fn embedded_type_name(&self, ty: Option>) -> Option> { + let mut ty = ty?; + if ty.kind() == "generic_type" { + ty = ty.child_by_field_name("type")?; + } + match ty.kind() { + "qualified_type" => ty.child_by_field_name("name"), + "type_identifier" if !is_go_predeclared_type(self.text(ty)) => Some(ty), + _ => None, + } + } + + /// extractInheritance — the Go branches: interface embedding (a type_elem + /// holding one named type; a union, `~T` or basic type is a constraint) + /// and struct embedding (field_declaration without a field_identifier), + /// plus the field_declaration_list recursion. fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) { stack_guard!(); let extends_kind = edge_kind_index("extends").unwrap(); for i in 0..node.named_child_count() { let Some(child) = node.named_child(i) else { continue }; match child.kind() { - "constraint_elem" => { - let type_id = (0..child.named_child_count()) + "type_elem" => { + let mut terms = (0..child.named_child_count()) .filter_map(|j| child.named_child(j)) - .find(|c| c.kind() == "type_identifier"); - if let Some(type_id) = type_id { + .filter(|c| c.kind() != "comment"); + let only = match (terms.next(), terms.next()) { + (Some(term), None) => Some(term), + _ => None, + }; + if let Some(type_id) = self.embedded_type_name(only) { let name = self.text(type_id).to_string(); self.push_ref_at(class_row, &name, extends_kind, type_id); } @@ -869,10 +891,7 @@ impl<'t> Walker<'t> { .filter_map(|j| child.named_child(j)) .any(|c| c.kind() == "field_identifier"); if !has_field_identifier { - let type_id = (0..child.named_child_count()) - .filter_map(|j| child.named_child(j)) - .find(|c| c.kind() == "type_identifier"); - if let Some(type_id) = type_id { + if let Some(type_id) = self.embedded_type_name(child.child_by_field_name("type")) { let name = self.text(type_id).to_string(); self.push_ref_at(class_row, &name, extends_kind, type_id); } @@ -1253,6 +1272,17 @@ fn is_builtin_type(name: &str) -> bool { ) } +/// GO_PREDECLARED_TYPES (languages/go.ts): never a node in the graph, and as +/// the lone term of an interface a basic type is a constraint, not an embedding. +fn is_go_predeclared_type(name: &str) -> bool { + matches!( + name, + "any" | "bool" | "byte" | "comparable" | "complex64" | "complex128" | "error" | "float32" + | "float64" | "int" | "int8" | "int16" | "int32" | "int64" | "rune" | "string" | "uint" + | "uint8" | "uint16" | "uint32" | "uint64" | "uintptr" + ) +} + fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef { match s { Some(s) => arena.put(s), diff --git a/src/extraction/languages/go.ts b/src/extraction/languages/go.ts index 5e41fbfaa..1bad7ff22 100644 --- a/src/extraction/languages/go.ts +++ b/src/extraction/languages/go.ts @@ -38,6 +38,32 @@ function extractGoReturnType(node: SyntaxNode, source: string): string | undefin return last; } +/** + * Go's predeclared types. None is ever a node in the graph, and as the lone + * term of an interface (`interface{ int64 }`) a basic type is a type-set + * constraint, not an embedded interface. + */ +const GO_PREDECLARED_TYPES: ReadonlySet = new Set([ + 'any', 'bool', 'byte', 'comparable', 'complex64', 'complex128', 'error', 'float32', 'float64', + 'int', 'int8', 'int16', 'int32', 'int64', 'rune', 'string', 'uint', 'uint8', 'uint16', 'uint32', + 'uint64', 'uintptr', +]); + +/** + * The name node of the type a Go embedding names, in a struct or an interface: + * `Base`, `*Base` (the `*` is a sibling token), `pkg.Base` and `Base[T]` all + * embed `Base`. A qualified type yields its name, not its package: the package + * stays in the source text, and resolution reads it back from the reference's + * position (#2322). Undefined for any other type (a literal, `~T`, a union's + * term) and for a predeclared one. + */ +export function goEmbeddedTypeName(type: SyntaxNode | null | undefined, source: string): SyntaxNode | undefined { + if (type?.type === 'generic_type') type = getChildByField(type, 'type'); + if (type?.type === 'qualified_type') return getChildByField(type, 'name') ?? undefined; + if (type?.type !== 'type_identifier') return undefined; + return GO_PREDECLARED_TYPES.has(getNodeText(type, source)) ? undefined : type; +} + export const goExtractor: LanguageExtractor = { functionTypes: ['function_declaration'], classTypes: [], // Go doesn't have classes diff --git a/src/extraction/tree-sitter.ts b/src/extraction/tree-sitter.ts index 9ea465cad..997d73517 100644 --- a/src/extraction/tree-sitter.ts +++ b/src/extraction/tree-sitter.ts @@ -23,6 +23,7 @@ import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types'; import { EXTRACTORS } from './languages'; import { stripCppTemplateArgs, isCppConstructorDeclaration } from './languages/c-cpp'; import { rustImplTypeName } from './languages/rust'; +import { goEmbeddedTypeName } from './languages/go'; import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeRefs } from './languages/dart'; import { LiquidExtractor } from './liquid-extractor'; import { RazorExtractor } from './razor-extractor'; @@ -3795,7 +3796,7 @@ export class TreeSitterExtractor { * contained by the interface (e.g. `Marshal`, `Unmarshal` of a `Core` * interface). tree-sitter-go names these `method_elem` (newer) or * `method_spec` (older). Embedded interfaces (`Reader` inside `ReadWriter`) - * are `type_identifier`s, not methods, and are left to inheritance extraction. + * are `type_elem`s, not methods, and are left to inheritance extraction. */ private extractGoInterfaceMethods(interfaceType: SyntaxNode, ifaceId: string): void { this.nodeStack.push(ifaceId); @@ -6984,15 +6985,18 @@ export class TreeSitterExtractor { } } - // Go interface embedding: `type Querier interface { LabelQuerier; ... }` - // constraint_elem wraps the embedded interface type identifier - if (child.type === 'constraint_elem') { - const typeId = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier'); + // Go interface embedding: `type IRouter interface { IRoutes; Group(…) }`. + // tree-sitter-go parses every element of an interface that is not a + // method as a `type_elem`; one holding a single named type embeds that + // interface. A union (`float32 | float64`), an underlying type (`~int`) + // or a lone basic type (`int64`) is a type-set constraint instead. + if (this.language === 'go' && child.type === 'type_elem') { + const terms = child.namedChildren.filter((c: SyntaxNode) => c.type !== 'comment'); + const typeId = terms.length === 1 ? goEmbeddedTypeName(terms[0], this.source) : undefined; if (typeId) { - const name = getNodeText(typeId, this.source); this.unresolvedReferences.push({ fromNodeId: classId, - referenceName: name, + referenceName: getNodeText(typeId, this.source), referenceKind: 'extends', line: typeId.startPosition.row + 1, column: typeId.startPosition.column, @@ -7001,25 +7005,23 @@ export class TreeSitterExtractor { } // Go struct embedding: field_declaration without field_identifier - // e.g. `type DB struct { *Head; Queryable }` — no field name means embedded type. + // e.g. `type DB struct { *Head; Queryable; sync.Mutex }` — no field name + // means an embedded type, read the way an interface's is. // Go only: C, C++ and Objective-C members are field_declarations too, but // nest their name inside the declarator (`Cache* cache_;`, `jv elems[];`, // `Status Get(int);`), so this test read every such member's type as a // supertype. Their supertypes come from base_class_clause / superclass. if (this.language === 'go' && child.type === 'field_declaration') { const hasFieldIdentifier = child.namedChildren.some((c: SyntaxNode) => c.type === 'field_identifier'); - if (!hasFieldIdentifier) { - const typeId = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier'); - if (typeId) { - const name = getNodeText(typeId, this.source); - this.unresolvedReferences.push({ - fromNodeId: classId, - referenceName: name, - referenceKind: 'extends', - line: typeId.startPosition.row + 1, - column: typeId.startPosition.column, - }); - } + const typeId = hasFieldIdentifier ? undefined : goEmbeddedTypeName(getChildByField(child, 'type'), this.source); + if (typeId) { + this.unresolvedReferences.push({ + fromNodeId: classId, + referenceName: getNodeText(typeId, this.source), + referenceKind: 'extends', + line: typeId.startPosition.row + 1, + column: typeId.startPosition.column, + }); } } diff --git a/src/resolution/frameworks/go.ts b/src/resolution/frameworks/go.ts index 5e819060e..b90d20dea 100644 --- a/src/resolution/frameworks/go.ts +++ b/src/resolution/frameworks/go.ts @@ -26,6 +26,15 @@ export const goResolver: FrameworkResolver = { }, resolve(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null { + // An embedded type is the one its package declares under that name: the + // same package's for `Node`, the import's for `pkg.Node`, which name + // matching and the import resolver work out. The patterns below pick among + // every same-named node by folder convention, and Pattern 4 sees structs + // only: `Node` embedded beside promql/parser's `type Node interface` went + // to discovery/kubernetes's struct `Node`, etcd's `Client` embedding its + // own `Lease` interface to the server's `Lease` struct. + if (ref.referenceKind === 'extends' || ref.referenceKind === 'implements') return null; + // Pattern 1: Handler references if (ref.referenceName.endsWith('Handler') || ref.referenceName.startsWith('Handle')) { const result = resolveByNameAndKind(ref, 'function', HANDLER_DIRS, context); diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 5da83c0a2..bf0917ad7 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -21,7 +21,7 @@ import { isImportableKind, CPP_DEFINE_SIGNATURE, } from './types'; -import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName } from './name-matcher'; +import { isPythonSelfCall, matchJsStoreBindingCall, isUnresolvedJsMemberCall, matchObjectPathCall, thisScopeCaller, isVisibleAcrossFiles, matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesCodeBoundary, gateLanguageMatch, dumpNameMatcherProfile, clearNameMatcherMemos, isRustNameInScope, CASE_INSENSITIVE_LANGUAGES, isDartMemberRead, matchDartMemberRead, isDartChainLink, matchDartChainLink, isDartAnnotation, matchDartAnnotation, isStdMethodName, isGoUnknownQualified } from './name-matcher'; import { isVisibleCppMacro, clearCppMacroVisibility } from './cpp-macro-visibility'; import { isCppConstructorRef, matchCppConstructor } from './cpp-constructor'; import { gateSwiftTypeTarget, clearSwiftTypeVisibility, swiftExtendedConformances } from './swift-type-visibility'; @@ -3056,6 +3056,11 @@ export class ReferenceResolver { result = { ...result, targetNodeId: type.id }; } if (isBoundToOutOfRepoImport(ref, this.context)) return null; + // A Go type embedded through a package (`yaml.Node`) is that package's. + // When the package is none of the file's imports as indexed, a type found + // by its bare name is some other package's namesake: prometheus's + // `RuleGroupNode` embeds yaml's `Node`, not discovery/kubernetes's. + if (ref.language === 'go' && isGoUnknownQualified(ref, this.context)) return null; return result; } diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index d86047ebd..794d2af01 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1883,7 +1883,35 @@ function rustModuleDir(filePath: string): string { return path.posix.join(dir, base.replace(/\.rs$/, '')); } -const GO_QUALIFIERS = new WeakMap>(); +interface GoQualification { + /** The package name written before the reference's name, as spelled. */ + written?: string; + /** The file's import that name is. */ + imported?: ImportMapping; +} + +const GO_QUALIFIERS = new WeakMap>(); + +function goRefQualification(ref: UnresolvedRef, context: ResolutionContext): GoQualification { + if (ref.referenceKind === 'imports') return {}; + const name = ref.referenceName.split('.').pop()!; + if (!/^[A-Za-z_]\w*$/.test(name)) return {}; + let memo = GO_QUALIFIERS.get(context); + if (!memo) GO_QUALIFIERS.set(context, (memo = new Map())); + const key = `${ref.filePath}\0${ref.line}\0${ref.column}\0${ref.referenceName}`; + const hit = memo.get(key); + if (hit !== undefined) return hit; + const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1] ?? ''; + const at = Math.max(0, ref.column); + // The qualifier right before the name at the reference's column, or the + // line's only spelling of the name. + const written = line.startsWith(name, at) ? /(?:^|[^\w.])([A-Za-z_]\w*)\.$/.exec(line.slice(0, at))?.[1] + : !new RegExp(`(? m.localName === written) : undefined; + const found = { written, imported }; + memo.set(key, found); + return found; +} /** * The import a Go reference is written through — `context` in @@ -1892,23 +1920,18 @@ const GO_QUALIFIERS = new WeakMap m.localName === before) : undefined; - memo.set(key, imported ?? null); - return imported; + return goRefQualification(ref, context).imported; +} + +/** + * Whether a Go reference is written through a package that is none of its + * file's imports as the index knows them: `yaml` in `yaml.Node` under an + * unaliased `import "go.yaml.in/yaml/v3"`, which is recorded under its last + * path element, `v3`. Which package that is cannot be told from here. + */ +export function isGoUnknownQualified(ref: UnresolvedRef, context: ResolutionContext): boolean { + const { written, imported } = goRefQualification(ref, context); + return written !== undefined && imported === undefined; } /**