mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 16:57:09 +08:00
fix(go): a struct links the types its fields are declared with (#2655)
A Go struct referenced nothing through its fields: etcd's `type
PutResponse struct { Header *ResponseHeader }` or gin's `type Context
struct { handlers HandlersChain }` had no `references` edge to those
types, so impact on `ResponseHeader` or `HandlersChain` missed every
struct holding one, and their users. Main had 0 such edges on gin, etcd,
prometheus and kubernetes; only embedded fields were read (`extends`).
TS extractor and Rust kernel, mirrored:
- goStructFieldTypeNames / struct_field_type_names walk each named
field's type and push a `references` ref per type name from the
struct, on the name. Fields mint no nodes: impact never climbs
`contains` upward (#536), so a field node would stop impact short of
the struct; Dart attributes field types to the class too (#2327).
- #2441's alias walk is factored into collectGoTypeNames /
collect_type_names + goDeclaredNames / declared_names, so struct
fields skip what aliases skip: predeclared types and, written bare,
the declaration's own type parameters and its own name (no self-edge,
no failed row for `next *node`).
- An embedded field stays `extends`; an embedded generic's type
arguments are references (client-go's
`listers.ResourceIndexer[*v1.Pod]`: 651 such fields in kubernetes).
Resolver: gateTargetKind's unknown-qualifier rule (#2417) now covers
every Go type declaration (GO_TYPE_KINDS). Without it, a field
`op clientv3.Op` under an unaliased go.etcd.io/etcd/client/v3 import
bound to the same file's own `Op`.
Explore: a `references` edge whose ends are both type declarations no
longer makes either end connected to an entry. With the new edges alone
the #1500 CG-10 gate failed: payroll's 8-line `RunResult` (holding a
`[]Payslip`) became connected and displaced the 51-line
runPayrollCycleAll body. The rule alone changes no rendered symbol on
gin, etcd, prometheus, kubernetes, bloc, bulletproof or refine-examples.
Validation (main de3eb71d, site-keyed A/B, every added edge judged
against real package clauses): gin +41, etcd +1006, prometheus +1637,
kubernetes +20203, all correct; 0 dropped/retargeted/metadata-only; no
node changes or self-loops; kernel == wasm. Index time within noise,
DB +0.4-1.4%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- 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 Go, a struct now links to the types its fields are declared with, like etcd's `PutResponse` holding a `*ResponseHeader` or gin's `Context` holding a `HandlersChain`, so impact on `ResponseHeader` or `HandlersChain` reaches every struct that holds one and the code that uses those structs, and `codegraph_node` lists a struct's field types. Before, a struct linked only to the types it embeds. A field written through another package, like `*pb.ResponseHeader`, links to that package's type, one from outside your project, like `context.Context`, links to nothing, the types an embedded generic is filled in with, like the `Pod` in client-go's `listers.ResourceIndexer[*v1.Pod]`, link too, and a struct that holds itself, like a list node's `next *node`, doesn't link to itself. `codegraph_explore` still answers a question about a flow with the code that runs it, not with the structs that merely hold one of its types. Re-index Go projects after upgrading.
|
||||
- In Go, a struct or defined type now counts as implementing an interface only when its methods take as many parameters and return as many results as the interface's. Before, matching method names was enough, so etcd's watch cache, whose `Get` takes a context, a key and options, was listed as a `peerGetter`, whose `Get` takes a peer ID, and a call through an interface could be shown reaching methods it can never reach. Look-alikes could also crowd out the real implementations, so `codegraph_explore` stopped at grpc-go's xDS `TransportBuilder` instead of reaching the transport it builds. A server still counts for the gRPC client interface of the service it serves, like etcd's `kvServer` for `KVClient`, so a client's call keeps leading to the handler that serves it. 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 `<Route {...rest}>`, 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 `<Link to="/counter">` links led nowhere. Each route links to the page its `element` renders, past a guard like `<ProtectedRoute>` or `<AdminWrapper>`, 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.
|
||||
|
||||
@@ -29,6 +29,39 @@ type Stack[T any] struct {
|
||||
items []T
|
||||
}
|
||||
|
||||
// A struct references the types its fields are declared with, but the types
|
||||
// it embeds (supertypes), its own type parameters, predeclared types and its
|
||||
// own name written bare.
|
||||
type Server struct {
|
||||
handlers HandlersChain
|
||||
byKey map[Dur][]*Widget `json:"by_key"`
|
||||
events <-chan pkga.Item
|
||||
grid [MAX_ITEMS]Widget
|
||||
cb func(ctx pkga.Context, w *Widget) (*Stack[int], error)
|
||||
a, b Widget
|
||||
nested struct {
|
||||
w Widget
|
||||
*Base
|
||||
}
|
||||
codec interface {
|
||||
Encode(pkga.Item) ([]byte, error)
|
||||
}
|
||||
*pkga.Lister[*Widget, pkga.Item]
|
||||
next *Server
|
||||
n int
|
||||
}
|
||||
|
||||
type Pool[T any] struct {
|
||||
items []T
|
||||
next *Pool[T]
|
||||
Stack[T]
|
||||
extra Widget
|
||||
}
|
||||
|
||||
type AnonFields = struct {
|
||||
w Widget
|
||||
}
|
||||
|
||||
// Units of time: the group's comment, no member's doc.
|
||||
type (
|
||||
Seconds int
|
||||
|
||||
@@ -188,8 +188,10 @@ describe('a Go defined type references the types it is defined from', () => {
|
||||
// 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).
|
||||
// type's own name written bare (stateFn, Tree). The struct Context
|
||||
// references its field's type.
|
||||
expect(typeRefs(result)).toEqual([
|
||||
'Context HandlersChain',
|
||||
'Generic List',
|
||||
'Grid Cell',
|
||||
'HandlerFunc Context',
|
||||
|
||||
@@ -0,0 +1,627 @@
|
||||
/**
|
||||
* A Go struct's fields referenced nothing: `type PutResponse struct { Header
|
||||
* *ResponseHeader }` (etcd) or gin's `type Context struct { handlers
|
||||
* HandlersChain }` was a `struct` node with no `references` edge to the types
|
||||
* its fields are declared with, so impact on `ResponseHeader` or
|
||||
* `HandlersChain` missed every struct that holds one, and everything that
|
||||
* uses those structs. Only embedded fields were read, as `extends` edges.
|
||||
*
|
||||
* The struct now references each type a named field's type names, on the
|
||||
* name, where resolution reads a package qualifier back. The references are
|
||||
* the struct's: Go fields mint no nodes, as Dart's don't (#2327), and impact
|
||||
* never climbs from a member to its container, so a field node would have
|
||||
* stopped impact short of the struct. Not references: an embedded field (an
|
||||
* `extends` edge already), the struct's own type parameters, Go's predeclared
|
||||
* types, and the struct's own name written bare (`next *node`), which names
|
||||
* the struct itself: no self-edge, and no unresolved row for the name.
|
||||
*
|
||||
* Resolution reads each as any type position: a bare name is its own
|
||||
* package's type, `pb.ResponseHeader` the import's, `context.Context` (a
|
||||
* package outside the project) nothing, and a name written through a package
|
||||
* the index doesn't know links to nothing either.
|
||||
*/
|
||||
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 shape a field's type can take, beside the declarations that skip names. */
|
||||
const FIELDS_SOURCE = `package etcdserverpb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
mvccpb "go.etcd.io/etcd/api/v3/mvccpb"
|
||||
"go.uber.org/zap"
|
||||
"k8s.io/client-go/gentype"
|
||||
)
|
||||
|
||||
type ResponseHeader struct {
|
||||
ClusterId uint64
|
||||
Revision int64
|
||||
}
|
||||
|
||||
// PutResponse is etcd's: a header and the previous key-value.
|
||||
type PutResponse struct {
|
||||
Header *ResponseHeader \`protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"\`
|
||||
PrevKv *mvccpb.KeyValue
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
mu sync.RWMutex
|
||||
lg *zap.Logger
|
||||
ctx context.Context
|
||||
handlers HandlersChain
|
||||
byKey map[Key][]*Watcher
|
||||
events <-chan Event
|
||||
grid [4][4]Cell
|
||||
cb func(ctx context.Context, req *Request) (*Response, error)
|
||||
wrapped (Cell)
|
||||
a, b Pair
|
||||
cache *List[Item]
|
||||
boxed mvccpb.Box[Item]
|
||||
conf struct {
|
||||
inner Inner
|
||||
*Base
|
||||
}
|
||||
codec interface {
|
||||
Encode(Message) ([]byte, error)
|
||||
}
|
||||
err error
|
||||
n int
|
||||
opts []any
|
||||
}
|
||||
|
||||
// Embedded fields are its supertypes, not references; the type arguments of
|
||||
// an embedded generic are references, as client-go's typed clients name the
|
||||
// resource they serve.
|
||||
type Embedder struct {
|
||||
*Base
|
||||
Queryable
|
||||
mvccpb.Embedded
|
||||
*mvccpb.Pointer \`json:"-"\`
|
||||
List[int]
|
||||
*gentype.ClientWithList[*Pod, *mvccpb.PodList]
|
||||
error
|
||||
name string
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
next *Node
|
||||
children map[string]*Node
|
||||
build func(*Node) error
|
||||
remote *mvccpb.Node
|
||||
}
|
||||
|
||||
type List[T any] struct {
|
||||
items []T
|
||||
head *elem[T]
|
||||
next *List[T]
|
||||
extra Extra
|
||||
}
|
||||
|
||||
type Pairs[K comparable, V any] struct {
|
||||
byKey map[K]V
|
||||
list List[V]
|
||||
List[K]
|
||||
*Box[Pairs[K, V]]
|
||||
}
|
||||
|
||||
type Anon = struct {
|
||||
w Widget
|
||||
}
|
||||
|
||||
type Empty struct{}
|
||||
`;
|
||||
|
||||
const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
|
||||
|
||||
/** `<kind> <name>` 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}`);
|
||||
}
|
||||
|
||||
/** `<struct> <name>` for every `references` ref out of a struct, sorted. */
|
||||
function fieldRefs(result: ExtractionResult): string[] {
|
||||
const byId = new Map(result.nodes.map((n) => [n.id, n]));
|
||||
return result.unresolvedReferences
|
||||
.filter((r) => r.referenceKind === 'references' && byId.get(r.fromNodeId)?.kind === 'struct')
|
||||
.map((r) => `${byId.get(r.fromNodeId)!.name} ${r.referenceName}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('a Go struct references the types its fields are declared with', () => {
|
||||
let savedEnv: Record<string, string | undefined> = {};
|
||||
|
||||
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 field mints no node of its own: %s${label}`, (backend) => {
|
||||
const result = extract(backend, 'rpc.pb.go', eol(FIELDS_SOURCE));
|
||||
expect(declarations(result)).toEqual([
|
||||
'struct ResponseHeader',
|
||||
'struct PutResponse',
|
||||
'struct Server',
|
||||
'struct Embedder',
|
||||
'struct Node',
|
||||
'struct List',
|
||||
'struct Pairs',
|
||||
'struct Anon',
|
||||
'struct Empty',
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(backends)(`a struct references each type its named fields name: %s${label}`, (backend) => {
|
||||
const source = eol(FIELDS_SOURCE);
|
||||
const result = extract(backend, 'rpc.pb.go', source);
|
||||
// Not references: the predeclared types (uint64, int64, error, int, any,
|
||||
// byte), the type parameters T, K and V, a struct's own name written bare
|
||||
// (Node, List, Pairs), and the type an embedded field embeds. A qualified
|
||||
// name keeps its package in the source (`zap.Logger`, `mvccpb.Node`), and
|
||||
// an anonymous struct or interface in a field is the field's type: its
|
||||
// fields, embedded or not, and its methods' types are the struct's.
|
||||
expect(fieldRefs(result)).toEqual([
|
||||
'Anon Widget',
|
||||
'Embedder Pod',
|
||||
'Embedder PodList',
|
||||
'List Extra',
|
||||
'List elem',
|
||||
'Node Node',
|
||||
'Pairs List',
|
||||
'PutResponse KeyValue',
|
||||
'PutResponse ResponseHeader',
|
||||
'Server Base',
|
||||
'Server Box',
|
||||
'Server Cell',
|
||||
'Server Cell',
|
||||
'Server Context',
|
||||
'Server Context',
|
||||
'Server Event',
|
||||
'Server HandlersChain',
|
||||
'Server Inner',
|
||||
'Server Item',
|
||||
'Server Item',
|
||||
'Server Key',
|
||||
'Server List',
|
||||
'Server Logger',
|
||||
'Server Message',
|
||||
'Server Pair',
|
||||
'Server RWMutex',
|
||||
'Server Request',
|
||||
'Server Response',
|
||||
'Server Watcher',
|
||||
]);
|
||||
// Each sits on its name, where resolution reads the qualifier back.
|
||||
const lines = source.split(/\r?\n/);
|
||||
const byId = new Map(result.nodes.map((n) => [n.id, n]));
|
||||
const refs = result.unresolvedReferences.filter((r) => r.referenceKind === 'references' && byId.get(r.fromNodeId)?.kind === 'struct');
|
||||
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 remote = refs.find((r) => byId.get(r.fromNodeId)!.name === 'Node')!;
|
||||
expect(lines[remote.line - 1]!.slice(0, remote.column).trimStart()).toBe('remote *mvccpb.');
|
||||
});
|
||||
|
||||
it.each(backends)(`an embedded field is still a supertype, and its type arguments references: %s${label}`, (backend) => {
|
||||
const result = extract(backend, 'rpc.pb.go', eol(FIELDS_SOURCE));
|
||||
const refsOf = (name: string) => {
|
||||
const struct = result.nodes.find((n) => n.kind === 'struct' && n.name === name)!;
|
||||
return result.unresolvedReferences
|
||||
.filter((r) => r.fromNodeId === struct.id)
|
||||
.map((r) => `${r.referenceKind} ${r.referenceName}`)
|
||||
.sort();
|
||||
};
|
||||
expect(refsOf('Embedder')).toEqual([
|
||||
'extends Base',
|
||||
'extends ClientWithList',
|
||||
'extends Embedded',
|
||||
'extends List',
|
||||
'extends Pointer',
|
||||
'extends Queryable',
|
||||
'references Pod',
|
||||
'references PodList',
|
||||
]);
|
||||
// `List[K]` and `*Box[Pairs[K, V]]` hold a type parameter or the struct
|
||||
// itself, and nothing else.
|
||||
expect(refsOf('Pairs')).toEqual(['extends Box', 'extends List', 'references List']);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/** The module of the indexed tests: etcd's and gin's shapes, with namesakes in a package that sorts first. */
|
||||
const MODULE: Record<string, string> = {
|
||||
'go.mod': 'module go.etcd.io/etcd\n\ngo 1.24\n',
|
||||
// A lookup by name meets these first.
|
||||
'alpha/names.go': [
|
||||
'package alpha',
|
||||
'',
|
||||
'type ResponseHeader struct{}',
|
||||
'',
|
||||
'type KeyValue struct{}',
|
||||
'',
|
||||
'type Context struct{}',
|
||||
'',
|
||||
'type Config struct{}',
|
||||
'',
|
||||
'type Client struct{}',
|
||||
'',
|
||||
'type Op struct{}',
|
||||
'',
|
||||
'type HandlersChain []int',
|
||||
'',
|
||||
'type Engine struct{}',
|
||||
'',
|
||||
'type node struct{}',
|
||||
'',
|
||||
'type elem struct{}',
|
||||
'',
|
||||
'type T struct{}',
|
||||
'',
|
||||
'type Pod struct{}',
|
||||
'',
|
||||
].join('\n'),
|
||||
// client-go's lister: the resource is a type argument of the embedded generic.
|
||||
'k8s/api/core/v1/types.go': [
|
||||
'package v1',
|
||||
'',
|
||||
'type Pod struct {',
|
||||
'\tName string',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'k8s/client-go/listers/generic.go': [
|
||||
'package listers',
|
||||
'',
|
||||
'type ResourceIndexer[T any] struct {',
|
||||
'\titems []T',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'k8s/client-go/listers/core/v1/pod.go': [
|
||||
'package v1',
|
||||
'',
|
||||
'import (',
|
||||
'\tcorev1 "go.etcd.io/etcd/k8s/api/core/v1"',
|
||||
'\t"go.etcd.io/etcd/k8s/client-go/listers"',
|
||||
')',
|
||||
'',
|
||||
'type podLister struct {',
|
||||
'\tlisters.ResourceIndexer[*corev1.Pod]',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'api/v3/etcdserverpb/rpc.pb.go': [
|
||||
'package etcdserverpb',
|
||||
'',
|
||||
'import mvccpb "go.etcd.io/etcd/api/v3/mvccpb"',
|
||||
'',
|
||||
'type ResponseHeader struct {',
|
||||
'\tRevision int64',
|
||||
'}',
|
||||
'',
|
||||
'type PutResponse struct {',
|
||||
'\tHeader *ResponseHeader',
|
||||
'}',
|
||||
'',
|
||||
'type RangeResponse struct {',
|
||||
'\tHeader *ResponseHeader',
|
||||
'\tKvs []*mvccpb.KeyValue',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'api/v3/mvccpb/kv.pb.go': [
|
||||
'package mvccpb',
|
||||
'',
|
||||
'type KeyValue struct {',
|
||||
'\tKey []byte',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'client/v3/client.go': [
|
||||
'package clientv3',
|
||||
'',
|
||||
'import (',
|
||||
'\t"context"',
|
||||
'',
|
||||
'\tpb "go.etcd.io/etcd/api/v3/etcdserverpb"',
|
||||
')',
|
||||
'',
|
||||
'// A namesake of context.Context in the same file.',
|
||||
'type Context struct{}',
|
||||
'',
|
||||
'type Config struct{}',
|
||||
'',
|
||||
'type Client struct {',
|
||||
'\tctx context.Context',
|
||||
'\tcfg Config',
|
||||
'\theader *pb.ResponseHeader',
|
||||
'}',
|
||||
'',
|
||||
'func (c *Client) Header() *pb.ResponseHeader { return c.header }',
|
||||
'',
|
||||
'func dial(c *Client) {}',
|
||||
'',
|
||||
].join('\n'),
|
||||
// Package clientv3, known to the index by its path (`v3`, `client`) only.
|
||||
'client/v3/ordering/kv.go': [
|
||||
'package ordering',
|
||||
'',
|
||||
'import "go.etcd.io/etcd/client/v3"',
|
||||
'',
|
||||
'type Op struct{}',
|
||||
'',
|
||||
'type kvOrdering struct {',
|
||||
'\tkv clientv3.Client',
|
||||
'\top clientv3.Op',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'gin/gin.go': [
|
||||
'package gin',
|
||||
'',
|
||||
'import "sync"',
|
||||
'',
|
||||
'type HandlerFunc func(*Context)',
|
||||
'',
|
||||
'type HandlersChain []HandlerFunc',
|
||||
'',
|
||||
'type Context struct {',
|
||||
'\thandlers HandlersChain',
|
||||
'\tengine *Engine',
|
||||
'}',
|
||||
'',
|
||||
'type Engine struct {',
|
||||
'\tpool sync.Pool',
|
||||
'\ttrees methodTrees',
|
||||
'}',
|
||||
'',
|
||||
'type methodTrees []methodTree',
|
||||
'',
|
||||
'type methodTree struct {',
|
||||
'\tmethod string',
|
||||
'\troot *node',
|
||||
'}',
|
||||
'',
|
||||
'type node struct {',
|
||||
'\tchildren []*node',
|
||||
'\thandlers HandlersChain',
|
||||
'}',
|
||||
'',
|
||||
'func (engine *Engine) handle(c *Context) {}',
|
||||
'',
|
||||
].join('\n'),
|
||||
'pkg/adt/list.go': [
|
||||
'package adt',
|
||||
'',
|
||||
'// A namesake of the type parameter in its own package.',
|
||||
'type T struct{}',
|
||||
'',
|
||||
'type List[T any] struct {',
|
||||
'\titems []T',
|
||||
'\thead *elem[T]',
|
||||
'}',
|
||||
'',
|
||||
'type elem[T any] struct {',
|
||||
'\tnext *elem[T]',
|
||||
'\tval T',
|
||||
'}',
|
||||
'',
|
||||
].join('\n'),
|
||||
};
|
||||
|
||||
describe.each([
|
||||
['default', 'LF'],
|
||||
['wasm', 'LF'],
|
||||
['default', 'CRLF'],
|
||||
['wasm', 'CRLF'],
|
||||
])('an indexed Go module links its struct fields’ 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-fields-'));
|
||||
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();
|
||||
};
|
||||
|
||||
/** `kind name` of each reference out of `name` in `file` that resolution left unresolved, sorted. */
|
||||
const unresolvedFrom = (file: string, name: string) =>
|
||||
cg!.getUnresolvedReferencesFrom(nodeNamed(file, name).id).map((r) => `${r.referenceKind} ${r.referenceName}`).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 struct references the types its fields name, each in its own package', () => {
|
||||
expect(linksFrom('api/v3/etcdserverpb/rpc.pb.go', 'PutResponse')).toEqual([
|
||||
'references api/v3/etcdserverpb/rpc.pb.go:ResponseHeader',
|
||||
]);
|
||||
expect(linksFrom('api/v3/etcdserverpb/rpc.pb.go', 'RangeResponse')).toEqual([
|
||||
'references api/v3/etcdserverpb/rpc.pb.go:ResponseHeader',
|
||||
'references api/v3/mvccpb/kv.pb.go:KeyValue',
|
||||
]);
|
||||
expect(linksFrom('gin/gin.go', 'Context')).toEqual([
|
||||
'references gin/gin.go:Engine',
|
||||
'references gin/gin.go:HandlersChain',
|
||||
]);
|
||||
expect(linksFrom('gin/gin.go', 'methodTree')).toEqual(['references gin/gin.go:node']);
|
||||
});
|
||||
|
||||
it('a name written through an import is that package’s type, and through an outside package nothing', () => {
|
||||
// `context.Context` is neither the same file's Context nor alpha's.
|
||||
expect(linksFrom('client/v3/client.go', 'Client')).toEqual([
|
||||
'references api/v3/etcdserverpb/rpc.pb.go:ResponseHeader',
|
||||
'references client/v3/client.go:Config',
|
||||
]);
|
||||
expect(unresolvedFrom('client/v3/client.go', 'Client')).toEqual(['references Context']);
|
||||
// `sync.Pool` is no project type.
|
||||
expect(linksFrom('gin/gin.go', 'Engine')).toEqual(['references gin/gin.go:methodTrees']);
|
||||
});
|
||||
|
||||
it('a struct naming itself has no edge to itself and no unresolved row for its name', () => {
|
||||
expect(linksFrom('gin/gin.go', 'node')).toEqual(['references gin/gin.go:HandlersChain']);
|
||||
expect(unresolvedFrom('gin/gin.go', 'node')).toEqual([]);
|
||||
});
|
||||
|
||||
it('an embedded generic is a supertype, and the types it is instantiated with references', () => {
|
||||
expect(linksFrom('k8s/client-go/listers/core/v1/pod.go', 'podLister')).toEqual([
|
||||
'extends k8s/client-go/listers/generic.go:ResourceIndexer',
|
||||
'references k8s/api/core/v1/types.go:Pod',
|
||||
]);
|
||||
expect(impactOf('k8s/api/core/v1/types.go', 'Pod')).toEqual([
|
||||
'k8s/api/core/v1/types.go:Pod',
|
||||
'k8s/client-go/listers/core/v1/pod.go:podLister',
|
||||
]);
|
||||
});
|
||||
|
||||
it('a generic struct’s own type parameters are no types it references', () => {
|
||||
expect(linksFrom('pkg/adt/list.go', 'List')).toEqual(['references pkg/adt/list.go:elem']);
|
||||
expect(linksFrom('pkg/adt/list.go', 'elem')).toEqual([]);
|
||||
expect(unresolvedFrom('pkg/adt/list.go', 'List')).toEqual([]);
|
||||
expect(unresolvedFrom('pkg/adt/list.go', 'elem')).toEqual([]);
|
||||
});
|
||||
|
||||
it('a name written through a package the index does not know links to nothing', () => {
|
||||
expect(linksFrom('client/v3/ordering/kv.go', 'kvOrdering')).toEqual([]);
|
||||
expect(unresolvedFrom('client/v3/ordering/kv.go', 'kvOrdering')).toEqual(['references Client', 'references Op']);
|
||||
});
|
||||
|
||||
it('impact on a type reaches the structs that hold it, and their users', () => {
|
||||
expect(impactOf('api/v3/etcdserverpb/rpc.pb.go', 'ResponseHeader')).toEqual([
|
||||
'api/v3/etcdserverpb/rpc.pb.go:PutResponse',
|
||||
'api/v3/etcdserverpb/rpc.pb.go:RangeResponse',
|
||||
'api/v3/etcdserverpb/rpc.pb.go:ResponseHeader',
|
||||
'client/v3/client.go:Client',
|
||||
'client/v3/client.go:Client::Header',
|
||||
'client/v3/client.go:dial',
|
||||
]);
|
||||
expect(impactOf('gin/gin.go', 'HandlersChain')).toEqual([
|
||||
'gin/gin.go:Context',
|
||||
'gin/gin.go:Engine::handle',
|
||||
'gin/gin.go:HandlerFunc',
|
||||
'gin/gin.go:HandlersChain',
|
||||
'gin/gin.go:methodTree',
|
||||
'gin/gin.go:methodTrees',
|
||||
'gin/gin.go:node',
|
||||
]);
|
||||
});
|
||||
|
||||
it('no Go field is a node', () => {
|
||||
for (const file of Object.keys(MODULE).filter((f) => f.endsWith('.go'))) {
|
||||
expect(cg!.getNodesInFile(file).filter((n) => n.kind === 'field'), file).toEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(['default', 'wasm'])('a field’s type declared after the index links on sync (%s)', (backend) => {
|
||||
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-fields-sync-'));
|
||||
fs.writeFileSync(path.join(root, 'go.mod'), 'module example.com/app\n\ngo 1.24\n');
|
||||
fs.mkdirSync(path.join(root, 'store'));
|
||||
fs.writeFileSync(path.join(root, 'store', 'store.go'), 'package store\n\ntype Store struct {\n\tidx *Index\n}\n');
|
||||
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;
|
||||
});
|
||||
|
||||
it('links the struct to the type once its file is added', async () => {
|
||||
const store = () => cg!.getNodesInFile('store/store.go').find((n) => n.name === 'Store')!;
|
||||
expect(cg!.getOutgoingEdgesFrom([store().id]).filter((e) => e.kind === 'references')).toEqual([]);
|
||||
fs.writeFileSync(path.join(root, 'store', 'index.go'), 'package store\n\ntype Index struct{}\n');
|
||||
await cg!.sync();
|
||||
const targets = cg!.getOutgoingEdgesFrom([store().id])
|
||||
.filter((e) => e.kind === 'references')
|
||||
.map((e) => `${cg!.getNode(e.target)!.filePath}:${cg!.getNode(e.target)!.name}`);
|
||||
expect(targets).toEqual(['store/index.go:Index']);
|
||||
}, 60_000);
|
||||
});
|
||||
@@ -199,7 +199,8 @@ describe('Go alias declarations are extracted', () => {
|
||||
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. The
|
||||
// defined types (WatchChan, Defined) reference theirs too.
|
||||
// defined types (WatchChan, Defined) reference theirs too, and a struct
|
||||
// its fields' (WatchResponse's `Events []*Event`).
|
||||
expect(typeRefs(result)).toEqual([
|
||||
'Defined WatchResponse',
|
||||
'Event Event',
|
||||
@@ -212,6 +213,7 @@ describe('Go alias declarations are extracted', () => {
|
||||
'Ptr WatchResponse',
|
||||
'Slice WatchResponse',
|
||||
'WatchChan WatchResponse',
|
||||
'WatchResponse Event',
|
||||
]);
|
||||
expectRefsOnTheirNames(result, source);
|
||||
});
|
||||
|
||||
+70
-24
@@ -581,6 +581,13 @@ impl<'t> Walker<'t> {
|
||||
// Struct embedding → extends (field_declaration without a
|
||||
// field_identifier), reached via the inheritance recursion.
|
||||
self.extract_inheritance(type_child, row);
|
||||
// The types its fields are declared with are the struct's
|
||||
// references: Go fields mint no nodes.
|
||||
let references = edge_kind_index("references").unwrap();
|
||||
for ty in self.struct_field_type_names(node) {
|
||||
let text = self.text(ty).to_string();
|
||||
self.push_ref_at(row, &text, references, ty);
|
||||
}
|
||||
let body = type_child.child_by_field_name("body").unwrap_or(type_child);
|
||||
for i in 0..body.named_child_count() {
|
||||
if let Some(c) = body.named_child(i) {
|
||||
@@ -897,6 +904,27 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
/// goDeclaredNames (languages/go.ts): what a type name written bare
|
||||
/// inside a type declaration can mean without naming another type — the
|
||||
/// declaration's own type parameters, and its own name, which in a
|
||||
/// recursive type is the declaration itself.
|
||||
fn declared_names(&self, decl: Node<'t>) -> HashSet<&'t str> {
|
||||
let mut names = HashSet::new();
|
||||
if let Some(name) = decl.child_by_field_name("name") {
|
||||
names.insert(self.text(name));
|
||||
}
|
||||
if let Some(list) = decl.child_by_field_name("type_parameters") {
|
||||
for param in (0..list.named_child_count()).filter_map(|i| list.named_child(i)) {
|
||||
for c in (0..param.named_child_count()).filter_map(|j| param.named_child(j)) {
|
||||
if c.kind() == "identifier" {
|
||||
names.insert(self.text(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
/// goAliasTypeNames (languages/go.ts): the name nodes of the types an
|
||||
/// 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
|
||||
@@ -905,36 +933,54 @@ impl<'t> Walker<'t> {
|
||||
/// around an error, never reaches the kernel (its file defers to wasm).
|
||||
fn alias_type_names(&self, node: Node<'t>) -> Vec<Node<'t>> {
|
||||
let mut names = Vec::new();
|
||||
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)) {
|
||||
for c in (0..decl.named_child_count()).filter_map(|j| decl.named_child(j)) {
|
||||
if c.kind() == "identifier" {
|
||||
params.insert(self.text(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ty) = node.child_by_field_name("type") {
|
||||
self.collect_type_names(ty, &self.declared_names(node), false, &mut names);
|
||||
}
|
||||
self.collect_alias_type_names(ty, ¶ms, own, false, &mut names);
|
||||
names
|
||||
}
|
||||
|
||||
/// `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<Node<'t>>,
|
||||
) {
|
||||
/// goStructFieldTypeNames (languages/go.ts): the name nodes of the types a
|
||||
/// struct's fields are declared with, in source order, skipped as in
|
||||
/// alias_type_names. An embedded field's type is a supertype
|
||||
/// (embedded_type_name), but the types it is instantiated with are not
|
||||
/// (`Pod` in client-go's `listers.ResourceIndexer[*v1.Pod]`).
|
||||
fn struct_field_type_names(&self, node: Node<'t>) -> Vec<Node<'t>> {
|
||||
let mut names = Vec::new();
|
||||
let fields = node.child_by_field_name("type").and_then(|ty| {
|
||||
(0..ty.named_child_count())
|
||||
.filter_map(|i| ty.named_child(i))
|
||||
.find(|c| c.kind() == "field_declaration_list")
|
||||
});
|
||||
let Some(fields) = fields else { return names };
|
||||
let declared = self.declared_names(node);
|
||||
for field in (0..fields.named_child_count()).filter_map(|i| fields.named_child(i)) {
|
||||
if field.kind() != "field_declaration" {
|
||||
continue;
|
||||
}
|
||||
let mut ty = field.child_by_field_name("type");
|
||||
// Embedded: no field name (extract_inheritance's test).
|
||||
let embedded = !(0..field.named_child_count())
|
||||
.filter_map(|j| field.named_child(j))
|
||||
.any(|c| c.kind() == "field_identifier");
|
||||
if embedded {
|
||||
ty = ty
|
||||
.filter(|t| t.kind() == "generic_type")
|
||||
.and_then(|t| t.child_by_field_name("type_arguments"));
|
||||
}
|
||||
if let Some(ty) = ty {
|
||||
self.collect_type_names(ty, &declared, false, &mut names);
|
||||
}
|
||||
}
|
||||
names
|
||||
}
|
||||
|
||||
/// collectGoTypeNames (languages/go.ts). `qualified`: the name of a
|
||||
/// `pkg.Name`, which is that package's whatever this one declares.
|
||||
fn collect_type_names(&self, node: Node<'t>, declared: &HashSet<&str>, qualified: bool, out: &mut Vec<Node<'t>>) {
|
||||
stack_guard!();
|
||||
if node.kind() == "type_identifier" {
|
||||
let text = self.text(node);
|
||||
if !is_go_predeclared_type(text) && (qualified || (!params.contains(text) && own != Some(text))) {
|
||||
if !is_go_predeclared_type(text) && (qualified || !declared.contains(text)) {
|
||||
out.push(node);
|
||||
}
|
||||
return;
|
||||
@@ -942,7 +988,7 @@ impl<'t> Walker<'t> {
|
||||
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, own, qualified, out);
|
||||
self.collect_type_names(c, declared, qualified, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,42 @@ export function goEmbeddedTypeName(type: SyntaxNode | null | undefined, source:
|
||||
return GO_PREDECLARED_TYPES.has(getNodeText(type, source)) ? undefined : type;
|
||||
}
|
||||
|
||||
/**
|
||||
* The names a type name written bare inside a Go type declaration can mean
|
||||
* without naming another type: the declaration's own type parameters, and
|
||||
* its own name, which in a recursive type is the declaration itself.
|
||||
*/
|
||||
function goDeclaredNames(decl: SyntaxNode, source: string): Set<string> {
|
||||
const names = new Set<string>();
|
||||
const nameNode = getChildByField(decl, 'name');
|
||||
if (nameNode) names.add(getNodeText(nameNode, source));
|
||||
for (const param of getChildByField(decl, 'type_parameters')?.namedChildren ?? []) {
|
||||
for (const c of param.namedChildren) {
|
||||
if (c.type === 'identifier') names.add(getNodeText(c, source));
|
||||
}
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the name nodes of the types `type` names onto `names`, in source
|
||||
* order, but Go's predeclared types and, written bare, the `declared` names
|
||||
* of the declaration it is written in.
|
||||
*/
|
||||
function collectGoTypeNames(type: SyntaxNode, declared: ReadonlySet<string>, source: string, names: 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 (!GO_PREDECLARED_TYPES.has(text) && (qualified || !declared.has(text))) names.push(n);
|
||||
return;
|
||||
}
|
||||
for (const c of n.namedChildren) walk(c, n.type === 'qualified_type');
|
||||
};
|
||||
walk(type, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`,
|
||||
@@ -80,26 +116,37 @@ export function goEmbeddedTypeName(type: SyntaxNode | null | undefined, source:
|
||||
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<string>();
|
||||
for (const decl of getChildByField(node, 'type_parameters')?.namedChildren ?? []) {
|
||||
for (const c of decl.namedChildren) {
|
||||
if (c.type === 'identifier') params.add(getNodeText(c, source));
|
||||
}
|
||||
}
|
||||
const names: SyntaxNode[] = [];
|
||||
// `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 (!GO_PREDECLARED_TYPES.has(text) && (qualified || (!params.has(text) && text !== own))) names.push(n);
|
||||
return;
|
||||
collectGoTypeNames(type, goDeclaredNames(node, source), source, names);
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name nodes of the types a Go struct's fields are declared with, in
|
||||
* source order: `ResponseHeader` in etcd's `Header *ResponseHeader`,
|
||||
* `HandlersChain` in gin's `handlers HandlersChain`, and every type a field's
|
||||
* function, map, channel or anonymous struct type names. `node` declares the
|
||||
* struct (a `type_spec`, or a `type_alias` of a struct literal). An embedded
|
||||
* field's type is a supertype (goEmbeddedTypeName), but the types it is
|
||||
* instantiated with are not: `Pod` in client-go's
|
||||
* `listers.ResourceIndexer[*v1.Pod]`. Skipped as in goAliasTypeNames: the
|
||||
* struct's own type parameters, the predeclared types, and its own name
|
||||
* written bare, which a linked list's `next *node` names.
|
||||
*/
|
||||
export function goStructFieldTypeNames(node: SyntaxNode, source: string): SyntaxNode[] {
|
||||
const fields = getChildByField(node, 'type')?.namedChildren.find((c: SyntaxNode) => c.type === 'field_declaration_list');
|
||||
const names: SyntaxNode[] = [];
|
||||
if (!fields) return names;
|
||||
const declared = goDeclaredNames(node, source);
|
||||
for (const field of fields.namedChildren) {
|
||||
if (field.type !== 'field_declaration') continue;
|
||||
let type = getChildByField(field, 'type');
|
||||
// Embedded: no field name (extractInheritance's test).
|
||||
if (type && !field.namedChildren.some((c: SyntaxNode) => c.type === 'field_identifier')) {
|
||||
type = type.type === 'generic_type' ? getChildByField(type, 'type_arguments') : null;
|
||||
}
|
||||
for (const c of n.namedChildren) walk(c, n.type === 'qualified_type');
|
||||
};
|
||||
walk(type, false);
|
||||
if (type) collectGoTypeNames(type, declared, source, names);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import { stripCppTemplateArgs, isCppConstructorDeclaration, cDeclaratorIdentifie
|
||||
import { NestedIntervals, scanCppBraceScopes, type CppBraceScopes } from './languages/cpp-brace-scopes';
|
||||
import { appendCppMacroMethods } from './languages/cpp-macro-methods';
|
||||
import { rustImplTypeName } from './languages/rust';
|
||||
import { goAliasTypeNames, goEmbeddedTypeName } from './languages/go';
|
||||
import { goAliasTypeNames, goEmbeddedTypeName, goStructFieldTypeNames } from './languages/go';
|
||||
import { dartMisparsedGenericCall, dartReceiverOf, isDartTypeName, pushDartTypeRefs } from './languages/dart';
|
||||
import { LiquidExtractor } from './liquid-extractor';
|
||||
import { RazorExtractor } from './razor-extractor';
|
||||
@@ -3846,6 +3846,20 @@ export class TreeSitterExtractor {
|
||||
if (typeChild) {
|
||||
// Extract struct embedding (e.g. Go: `type DB struct { *Head; Queryable }`)
|
||||
this.extractInheritance(typeChild, aggregateNode.id);
|
||||
// A Go struct references the types its fields are declared with, as
|
||||
// the struct: Go fields mint no nodes, and impact never climbs from a
|
||||
// member to its container (Dart's fields are the same, #2327).
|
||||
if (this.language === 'go') {
|
||||
for (const type of goStructFieldTypeNames(node, this.source)) {
|
||||
this.unresolvedReferences.push({
|
||||
fromNodeId: aggregateNode.id,
|
||||
referenceName: getNodeText(type, this.source),
|
||||
referenceKind: 'references',
|
||||
line: type.startPosition.row + 1,
|
||||
column: type.startPosition.column,
|
||||
});
|
||||
}
|
||||
}
|
||||
const body = getChildByField(typeChild, this.extractor.bodyField) || typeChild;
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
const child = body.namedChild(i);
|
||||
|
||||
+10
-2
@@ -4513,9 +4513,18 @@ export class ToolHandler {
|
||||
const fileGroups = new Map<string, { nodes: Node[]; score: number; peripheral: number }>();
|
||||
const entryNodeIds = new Set([...subgraph.roots, ...namedSeedIds]);
|
||||
|
||||
// Build a set of nodes directly connected to entry points (depth 1)
|
||||
// Build a set of nodes directly connected to entry points (depth 1). A
|
||||
// type that names another type in its declaration — a Go struct's field,
|
||||
// the type an alias or a defined type names, a Dart class's field — is
|
||||
// structure, not one hop of the question's flow: connected through it, the
|
||||
// payroll fixture's 8-line `RunResult` (which holds a `Payslip`) outranked
|
||||
// the 51-line method that builds and stores every payslip, and took its
|
||||
// place in the file's budget.
|
||||
const TYPE_KINDS = new Set(['class', 'struct', 'union', 'interface', 'trait', 'protocol', 'enum', 'type_alias']);
|
||||
const isTypeDeclaration = (id: string): boolean => TYPE_KINDS.has(subgraph.nodes.get(id)?.kind ?? '');
|
||||
const connectedToEntry = new Set<string>();
|
||||
for (const edge of subgraph.edges) {
|
||||
if (edge.kind === 'references' && isTypeDeclaration(edge.source) && isTypeDeclaration(edge.target)) continue;
|
||||
if (entryNodeIds.has(edge.source)) connectedToEntry.add(edge.target);
|
||||
if (entryNodeIds.has(edge.target)) connectedToEntry.add(edge.source);
|
||||
}
|
||||
@@ -4585,7 +4594,6 @@ export class ToolHandler {
|
||||
// displaces a flow-central file. Bounded: only the few named seeds, only the
|
||||
// types in their signatures.
|
||||
const CALLABLE_KINDS = new Set(['method', 'function', 'component', 'constructor']);
|
||||
const TYPE_KINDS = new Set(['class', 'struct', 'union', 'interface', 'trait', 'protocol', 'enum', 'type_alias']);
|
||||
const SIG_EDGE = new Set(['references', 'type_of', 'returns']);
|
||||
const changeSurfaceCandidates: Node[] = [];
|
||||
const seenChangeSurface = new Set<string>();
|
||||
|
||||
@@ -3324,11 +3324,13 @@ export class ReferenceResolver {
|
||||
return target && !isImportableKind(target.kind) ? null : result;
|
||||
}
|
||||
|
||||
// A Go alias names its type as an embedding does (below): written through
|
||||
// a package that is none of the file's imports as indexed, the type found
|
||||
// by its bare name is another package's namesake, or the alias itself.
|
||||
// A Go type declaration — an alias, a defined type, a struct's fields —
|
||||
// names its types as an embedding does (below): written through a package
|
||||
// that is none of the file's imports as indexed, the type found by its
|
||||
// bare name is another package's namesake, or the declaration itself
|
||||
// (`op clientv3.Op` beside the file's own `Op`).
|
||||
if (ref.language === 'go' && ref.referenceKind === 'references' &&
|
||||
this.nodeById(ref.fromNodeId)?.kind === 'type_alias' && isGoUnknownQualified(ref, this.context)) return null;
|
||||
GO_TYPE_KINDS.has(this.nodeById(ref.fromNodeId)?.kind ?? '') && isGoUnknownQualified(ref, this.context)) return null;
|
||||
|
||||
// A Go type position — a parameter or result type, a composite literal's
|
||||
// type — names a type of the package Go reads it from (route handlers are
|
||||
|
||||
Reference in New Issue
Block a user