mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(go): a defined type links the types it is defined from (#2441)
A Go defined type (`type WatchChan <-chan WatchResponse`, gin's `type HandlerFunc func(*Context)` and `type HandlersChain []HandlerFunc`) is a `type_alias` node, and it referenced nothing: only an `=` alias (#2417) named the types on its right-hand side. Impact on `WatchResponse` or gin's `Context` missed the declarations built from them, and everything that uses those. Fix (TS extractor and the Rust kernel, mirrored): goAliasTypeNames / alias_type_names walk the `type` field of every type_alias node, a defined type's as well as an alias's, so each type it names becomes a `references` ref on that name, where resolution reads the package qualifier back. Still skipped: the declaration's own type parameters, Go's predeclared types, and now its own name written bare, which in a recursive type (prometheus' `type stateFn func(*Lexer) stateFn`) is the declaration itself: no self-edge, and no failed row that would make dead code treat every namesake as referenced. A qualified name is never skipped (`type PutResponse pb.PutResponse` names pb's). Resolution is unchanged. A defined type is a new type: goAliasTarget still follows `=` aliases only, so a method called on a defined type is not looked up on its underlying type. The gateTargetKind rule that leaves a type_alias's reference written through a package the index doesn't know unresolved now covers defined types too; without it, every such name bound to the same-file declaration of its name (`type Op clientv3.Op` linked itself). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 `<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.
|
||||
- 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)
|
||||
|
||||
@@ -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{}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/** `<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}`);
|
||||
}
|
||||
|
||||
/** `<source> <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<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 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<string, string> = {
|
||||
'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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
+21
-12
@@ -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<Node<'t>> {
|
||||
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<Node<'t>>) {
|
||||
/// `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>>,
|
||||
) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user