mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-21 08:47:04 +08:00
fix(go): a call on a package-level variable or constant links through the type it is declared with (#2667)
Nothing read the declaration of a Go variable or constant declared at the
top level of a package (the scope reader skips them, and the pattern scan
reads only the calling function), so a method call through one was matched
by the method's name: etcd's `var lg *zap.Logger` made `lg.Panic(...)` a
project logger's `Panic`, prometheus's protobuf `xxx_messageInfo_*` values
reached project `Marshal` methods, and kubernetes' registry tests' calls on
each package's `var Strategy = fooStrategy{...}` reached another package's
exported `Strategy` type.
matchGoPackageValueCall reads a same-package `variable` or `constant` node's
spec when the call's receiver is a plain name that no local or parameter
binds and no import names: a written type, a composite literal's or a
conversion's type, or for a constant alone in a `const ( ... )` group the
spec above it. A project type gives its own method (0.9) through the
package that declares it; a type or value from outside the project, a
predeclared type or a type literal gives nothing. A value a project
function returns, an untyped constant, declarations that differ between
build-tag files, and a project type whose methods aren't found are left to
the strategies below. A `package foo_test` file in the directory is another
package.
A/B on main 90d6501b, same kernel for both arms, full index per arm:
- gin: 4 guesses now typed at 0.9.
- prometheus: 59 dropped (gogo-protobuf `xxx_messageInfo_*`, `sync.Pool`
values), 40 new links (`RoleEndpoint.String()` and its siblings).
- etcd: 18 dropped (`var lg *zap.Logger`, a cobra command), 43 new links
(`var display printer = &simplePrinter{}`), 3 now typed.
- harbor: 2 dropped (`var handler http.Handler`), 24 new, 38 retargeted
(`var configManager *cfgPkg.CfgManager`), 34 now typed.
- kubernetes: 46 dropped (outside-typed values), 260 new, 405 retargeted
(each registry package's own strategy type), 430 now typed.
fmt, llmc and typeorm are identical. Every dropped link, grouped by
receiver, and every retargeted owner pair was checked against the
receiver's declaration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -149,6 +149,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Go, a call is now matched to the package or variable it is actually made on. Before, a call through a package, like `klog.Infof("Log using Infof")` or `digest.Digest(dig)` inside `&distribution.Descriptor{Digest: …}`, could link to a project method of the same name when the name appeared again on the line, and in `klog.Error(err.Error())` the `err.Error()` call was taken for a call into `klog`. A parameter, receiver or local variable named like an import, like etcd's `jwt, err := newTokenProviderJWT(…)` or the `suite` receiver in a testify test suite, now counts as that variable, so calls on it link to the methods of the type it is declared as, and to nothing when that type comes from outside your project. Re-index Go projects after upgrading.
|
||||
- In Go, a call through a parameter or local variable named like a standard-library package, such as `context`, `log`, `trace`, `printer` or `token`, now links to the method it calls. Before, every call written `context.…` or `printer.…` was taken for a call into the standard library and linked to nothing: gin's `context.AbortWithError(…)` inside `func(context *Context)`, harbor's `context.IsAuthenticated()` after `context := NewSecurityContext(…)` and `log.Errorf(…)` after `log := log.G(ctx)`, etcd's `printer.DBHashKV(…)` and `trace.Step(…)`. The variable's declaration decides the method: the type it is declared as, or the package of the project function its value comes from. A variable holding a value from outside your project, like `scanner := bufio.NewScanner(r)` or `url, err := url.Parse(raw)`, still links to nothing, and one whose type can't be told links only to a method no other type in the project has, never to one picked because it matches the variable's name. Re-index Go projects after upgrading.
|
||||
- In Go, a call of a method name that nearly every type has, like `err.Error()`, `id.String()`, `ctx.Err()` or `mu.Lock()`, now links to that method of the type its variable is declared as, and to nothing when that type is built into Go, like `error` or `string`, or comes from outside your project, like `context.Context`, `sync.Mutex` or `testing.TB`. Before, when the variable's type wasn't worked out, such a call was matched by a word its name shares with a type's, so kubernetes' `err.Error()` calls linked to an unrelated `notRegisteredErr` type and etcd's `leaderID.String()` to `MoveLeaderRequest`. A call on a variable whose type still can't be told now links only to a type the variable is named after, like `id` for an `ID`. Re-index Go projects after upgrading.
|
||||
- In Go, a method call on a variable or constant declared at the top level of a package, like `lg.Panic(…)` after `var lg *zap.Logger` or `RoleEndpoint.String()` after `RoleEndpoint Role = "endpoints"`, now links to that method of the type it is declared with, from any file of the package, and to nothing when that type, or the value it is set to, comes from outside your project. Before, nothing read such a declaration, so the call was matched by its method's name alone and could link to an unrelated type's method, like a project logger's `Panic` or another package's `Role`. Re-index Go projects after upgrading.
|
||||
- React Router routes written as JSX now read `<Route index>` the way route objects do: inside `<Route path="/" element={<Layout />}>`, `<Route index element={<Home />} />` is the page at `/`, and `Layout` counts as the layout of every route nested in it, as does a guard with no path of its own like `<Route element={<RequireAuth />}>`. Before, the index route was never read, so `/` led to the layout itself and a `<Route path="/dashboard">` holding only an index route led to nothing. A route's `element` written on lines of its own, the way Prettier formats a long one, or wrapped in `<Suspense fallback={<Loader />}>`, now links to the page it shows rather than to nothing or to the fallback, and a path written in braces, like `path={"agents"}`, is read like any other. Re-index React projects after upgrading.
|
||||
- In JavaScript and TypeScript, a module loaded with `require('./x').default` or `const { default: X } = require('./x')` now links to its default export when it is written as an ES module: bitwarden's desktop app loads `export default class OsBiometricsServiceMac` this way before calling `new OsBiometricsServiceMac(…)`, and React Native's own libraries load many of their modules like this. Before, `.default` was looked up only as an export named `default`, which `export default` is not, so the `new` and the calls made through it linked to nothing, to the local variable holding the module, or to a method of the same name somewhere else. A Svelte component loaded this way and created with `new App({ target })` now links to the component. A CommonJS module that sets `exports.default` itself, or both `module.exports` and `module.exports.default` as fastify does, still links to what it sets, and one that sets no `default` still links to nothing. Re-index JavaScript and TypeScript projects after upgrading.
|
||||
- A React Router route that lazily loads a folder, like `lazy: () => import('@/pages/invocation_page')`, now links to the page the folder's `index.ts` passes on, whether it writes `export * from './invocation_page'`, `export { default } from './page'` or `export { Page as Component } from './page'`, even through another `index.ts`. A route module that names its page with `export { Page as Component }` links to it as well, and so does a page loaded with `lazy(() => import(…))` through such an `index.ts`. Before, these routes linked to nothing, so their pages had no route leading to them. Re-index React projects after upgrading.
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Go calls through a variable or constant a package declares at the top
|
||||
* level — `lg.Panic(…)` after `var lg *zap.Logger`, `RoleEndpoint.String()`
|
||||
* after `RoleEndpoint Role = "endpoints"` — read by that declaration, in the
|
||||
* file or another of the package, as a local's is: the method of the project
|
||||
* type it is declared as, or nothing for a type or value from outside the
|
||||
* project. Before, nothing read such a declaration, and the call was guessed
|
||||
* by its method's name: etcd's `lg.Panic(…)` reached a project logger's
|
||||
* `Panic`, prometheus's `RoleEndpoint.String()` another package's `Role`.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { CodeGraph } from '../src';
|
||||
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-go-pkg-values-'));
|
||||
const files: Record<string, string> = {
|
||||
'go.mod': 'module example.com/app\n\ngo 1.22\n',
|
||||
'logx/logger.go': `package logx
|
||||
|
||||
type Logger struct{}
|
||||
|
||||
func New() *Logger { return &Logger{} }
|
||||
|
||||
func (l *Logger) Panic(msg string) {}
|
||||
|
||||
func (l *Logger) Info(msg string) {}
|
||||
`,
|
||||
'tools/dump.go': `package tools
|
||||
|
||||
import "go.uber.org/zap"
|
||||
|
||||
var lg *zap.Logger
|
||||
|
||||
func dump() {
|
||||
lg.Panic("boom")
|
||||
}
|
||||
`,
|
||||
'tools/other.go': `package tools
|
||||
|
||||
func other() {
|
||||
lg.Info("other file")
|
||||
}
|
||||
`,
|
||||
'tools/shadow.go': `package tools
|
||||
|
||||
import "example.com/app/logx"
|
||||
|
||||
func shadow() {
|
||||
lg := logx.New()
|
||||
lg.Panic("local")
|
||||
}
|
||||
`,
|
||||
'discovery/kubernetes/kubernetes.go': `package kubernetes
|
||||
|
||||
type Role string
|
||||
|
||||
func (r Role) String() string { return string(r) }
|
||||
|
||||
const (
|
||||
RoleNode Role = "node"
|
||||
RolePod Role = "pod"
|
||||
)
|
||||
`,
|
||||
'discovery/kubernetes/metrics.go': `package kubernetes
|
||||
|
||||
func nodeRole() string {
|
||||
return RoleNode.String()
|
||||
}
|
||||
`,
|
||||
'discovery/kubernetes/kind.go': `package kubernetes
|
||||
|
||||
type Kind int
|
||||
|
||||
const (
|
||||
KindA Kind = iota
|
||||
// KindB repeats KindA's type.
|
||||
KindB
|
||||
KindC
|
||||
)
|
||||
|
||||
func (k Kind) Label() string { return "" }
|
||||
|
||||
func kindLabel() string {
|
||||
return KindB.Label()
|
||||
}
|
||||
`,
|
||||
'discovery/kubernetes/roles.go': `package kubernetes
|
||||
|
||||
var podRole = Role("pod")
|
||||
|
||||
func podRoleName() string {
|
||||
return podRole.String()
|
||||
}
|
||||
`,
|
||||
'discovery/aws/aws.go': `package aws
|
||||
|
||||
type Role string
|
||||
|
||||
func (r Role) String() string { return string(r) }
|
||||
`,
|
||||
'tags/tag.go': `package tags
|
||||
|
||||
type Tag struct{}
|
||||
|
||||
func (t Tag) Label() string { return "" }
|
||||
`,
|
||||
'registry/registry.go': `package registry
|
||||
|
||||
type Registry struct{}
|
||||
|
||||
func (r *Registry) Register(name string) {}
|
||||
|
||||
var defaultRegistry = &Registry{}
|
||||
|
||||
func register() {
|
||||
defaultRegistry.Register("x")
|
||||
}
|
||||
`,
|
||||
'plugins/manager.go': `package plugins
|
||||
|
||||
type Manager struct{}
|
||||
|
||||
func (m *Manager) Register(name string) {}
|
||||
`,
|
||||
'buffers/buffers.go': `package buffers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
var buf = bytes.NewBuffer(nil)
|
||||
|
||||
func flush(w io.Writer) {
|
||||
buf.WriteTo(w)
|
||||
}
|
||||
`,
|
||||
'sinks/sink.go': `package sinks
|
||||
|
||||
import "io"
|
||||
|
||||
type Sink struct{}
|
||||
|
||||
func (s *Sink) WriteTo(w io.Writer) (int64, error) { return 0, nil }
|
||||
`,
|
||||
'parsers/parser.go': `package parsers
|
||||
|
||||
type Parser struct{}
|
||||
|
||||
func NewParser() *Parser { return &Parser{} }
|
||||
|
||||
func (p *Parser) ParseExpr(s string) {}
|
||||
`,
|
||||
'codecs/codec.go': `package codecs
|
||||
|
||||
type Encoder interface {
|
||||
Encode() string
|
||||
}
|
||||
|
||||
type Serializer interface {
|
||||
Encoder
|
||||
}
|
||||
|
||||
type Codec Serializer
|
||||
|
||||
type jsonCodec struct{}
|
||||
|
||||
func (jsonCodec) Encode() string { return "" }
|
||||
|
||||
var JSONCodec Codec = jsonCodec{}
|
||||
|
||||
func encode() string {
|
||||
return JSONCodec.Encode()
|
||||
}
|
||||
`,
|
||||
'parsers/use.go': `package parsers
|
||||
|
||||
var testParser = NewParser()
|
||||
|
||||
func parse() {
|
||||
testParser.ParseExpr("x")
|
||||
}
|
||||
`,
|
||||
};
|
||||
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 });
|
||||
}, 120_000);
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** `file::qualifiedName resolvedBy@confidence` of every method the function `name` in `file` calls. */
|
||||
function calls(file: string, name: string): string[] {
|
||||
const fn = cg.getNodesInFile(file).find((n) => n.name === name && n.kind !== 'method');
|
||||
expect(fn, name).toBeDefined();
|
||||
return cg
|
||||
.getOutgoingEdges(fn!.id)
|
||||
.filter((e) => e.kind === 'calls')
|
||||
.map((e) => ({ e, n: cg.getNode(e.target)! }))
|
||||
.filter(({ n }) => n.kind === 'method')
|
||||
.map(({ e, n }) => `${n.filePath.replace(/\\/g, '/')}::${n.qualifiedName} ${String(e.metadata?.resolvedBy)}@${String(e.metadata?.confidence)}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
describe('Go calls through a package-level variable or constant', () => {
|
||||
it.each([
|
||||
// `var lg *zap.Logger`: not the project's only `Panic`.
|
||||
['tools/dump.go', 'dump', 'a variable of a type from outside the project'],
|
||||
// The same `lg`, declared in another file of the package.
|
||||
['tools/other.go', 'other', 'a variable another file declares'],
|
||||
// `var buf = bytes.NewBuffer(nil)`: not the project's only `WriteTo`.
|
||||
['buffers/buffers.go', 'flush', 'a value a package from outside the project hands out'],
|
||||
])('links nothing from %s %s: %s', (file, fn) => {
|
||||
expect(calls(file, fn)).toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
// `RoleNode Role = "node"` in kubernetes.go, not aws's `Role`.
|
||||
['discovery/kubernetes/metrics.go', 'nodeRole', 'discovery/kubernetes/kubernetes.go::Role::String instance-method@0.9'],
|
||||
// `KindB`, alone in its group, has `Kind` from `KindA Kind = iota`.
|
||||
['discovery/kubernetes/kind.go', 'kindLabel', 'discovery/kubernetes/kind.go::Kind::Label instance-method@0.9'],
|
||||
// `var defaultRegistry = &Registry{}`, not a plugin `Manager`.
|
||||
['registry/registry.go', 'register', 'registry/registry.go::Registry::Register instance-method@0.9'],
|
||||
// `var podRole = Role("pod")`.
|
||||
['discovery/kubernetes/roles.go', 'podRoleName', 'discovery/kubernetes/kubernetes.go::Role::String instance-method@0.9'],
|
||||
])('links %s %s through the declared type', (file, fn, target) => {
|
||||
expect(calls(file, fn)).toEqual([target]);
|
||||
});
|
||||
|
||||
it('leaves a local of the name, a value a project function returns and a type whose methods it can\'t see as they were', () => {
|
||||
// `lg := logx.New()` shadows the package's `lg`.
|
||||
expect(calls('tools/shadow.go', 'shadow').filter((c) => c.startsWith('logx/logger.go::Logger::Panic '))).toHaveLength(1);
|
||||
// `var testParser = NewParser()` says no type.
|
||||
expect(calls('parsers/use.go', 'parse').filter((c) => c.startsWith('parsers/parser.go::Parser::ParseExpr '))).toHaveLength(1);
|
||||
// `var JSONCodec Codec = jsonCodec{}`: `Codec` is defined over an
|
||||
// interface, whose methods its node doesn't carry.
|
||||
expect(calls('codecs/codec.go', 'encode')).not.toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -13106,6 +13106,7 @@ export function clearNameMatcherMemos(context: ResolutionContext): void {
|
||||
SCALA_OBJECT_PACKAGES.delete(context);
|
||||
GO_QUALIFIERS.delete(context);
|
||||
GO_SCOPE_INDEXES.delete(context);
|
||||
GO_PACKAGE_CLAUSES.delete(context);
|
||||
GO_EMBEDS.delete(context);
|
||||
GO_ALIAS_TARGETS.delete(context);
|
||||
GO_DOT_IMPORTS.delete(context);
|
||||
@@ -14277,6 +14278,13 @@ export function matchMethodCall(
|
||||
if (value !== undefined) return value && matchGoStdlibNamedLocalCall(objectOrClass!, methodName!, value, ref, context);
|
||||
}
|
||||
|
||||
// A Go call through a variable or constant its package declares at the top
|
||||
// level, in this file or another, is read by that declaration.
|
||||
if (ref.language === 'go' && dotMatch && !objectOrClass!.includes('.')) {
|
||||
const packageValue = matchGoPackageValueCall(objectOrClass!, methodName!, ref, context);
|
||||
if (packageValue !== undefined) return packageValue;
|
||||
}
|
||||
|
||||
// A Dart call through a type's name is to that type's own member, or to
|
||||
// nothing in the project.
|
||||
if (ref.language === 'dart' && dotMatch) {
|
||||
@@ -15351,6 +15359,143 @@ function matchGoStdlibNamedLocalCall(
|
||||
? { original: ref, targetNodeId: methods[0]!.id, confidence: 0.7, resolvedBy: 'instance-method' } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Go call through a variable or constant its package declares at the top
|
||||
* level — `lg.Panic(…)` after `var lg *zap.Logger`, `RoleEndpoint.String()`
|
||||
* after `RoleEndpoint Role = "endpoints"`, in this file or another of the
|
||||
* package — read by that declaration, as a local's is: the method of the
|
||||
* project type it is declared as, or nothing for a type from outside the
|
||||
* project, a predeclared type, a type literal or a value a package from
|
||||
* outside the project hands out. etcd's `var lg *zap.Logger` made each
|
||||
* `lg.Panic(…)` a project logger's `Panic`, and prometheus's
|
||||
* `RoleEndpoint.String()` reached another package's `Role`. Undefined, for
|
||||
* the strategies below, when the name is no such declaration where the call
|
||||
* is — a local or parameter binds it, the file imports a package by it — when
|
||||
* the declaration says no type (a value a project function returns), or when
|
||||
* no method of the project type it names is found.
|
||||
*/
|
||||
function matchGoPackageValueCall(receiver: string, method: string, ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
|
||||
const dir = goPackageDir(ref.filePath);
|
||||
const decls = context.getNodesByName(receiver).filter((n) =>
|
||||
n.language === 'go' && (n.kind === 'variable' || n.kind === 'constant') && goPackageDir(n.filePath) === dir);
|
||||
if (decls.length === 0) return undefined;
|
||||
if (context.getImportMappings(ref.filePath, 'go').some((m) => m.localName === receiver)) return undefined;
|
||||
if (goLocalDecl(receiver, ref, context)) return undefined;
|
||||
// A package's external tests (`package foo_test`) share its directory.
|
||||
const pkg = goPackageClause(ref.filePath, context);
|
||||
let type: GoPackageValueType | undefined;
|
||||
for (const d of decls) {
|
||||
if (goPackageClause(d.filePath, context) !== pkg) continue;
|
||||
const t = goPackageValueType(d, context);
|
||||
// Files for other build targets may declare it differently.
|
||||
if (t === undefined || (type !== undefined && (type?.written.replace(/\s/g, '') ?? null) !== (t?.written.replace(/\s/g, '') ?? null))) {
|
||||
return undefined;
|
||||
}
|
||||
type = t;
|
||||
}
|
||||
if (type === undefined) return undefined;
|
||||
if (type === null) return null;
|
||||
const named = goNamedType(type.written);
|
||||
if (!named || isGoOutsideType(named, type.file, context)) return null;
|
||||
const dot = named.indexOf('.');
|
||||
const name = named.slice(dot + 1);
|
||||
if (dot < 0 && GO_BUILTIN_FIELD_TYPES.has(name)) return null;
|
||||
const pkgDir = dot < 0 ? goPackageDir(type.file) : goImportPackageDir(named.slice(0, dot), type.file, context);
|
||||
if (!pkgDir || goPackageTypes(name, pkgDir, context).length === 0) return undefined;
|
||||
// A project type whose methods aren't found is left to the strategies
|
||||
// below: kubernetes' `var UnstructuredJSONScheme runtime.Codec =
|
||||
// unstructuredJSONScheme{}` is a `Codec`, defined over an interface whose
|
||||
// methods its node doesn't carry.
|
||||
return resolveMethodOnType(name, method, ref, context, 0.9, 'instance-method', pkgDir) ?? undefined;
|
||||
}
|
||||
|
||||
/** A Go package-level value's type as its declaration writes it, with the declaring file; null for one with no project method. */
|
||||
type GoPackageValueType = { written: string; file: string } | null;
|
||||
|
||||
/**
|
||||
* The type a Go package-level variable or constant `n` is declared with,
|
||||
* read from its spec: `*zap.Logger` in `var lg *zap.Logger`, `Role` in
|
||||
* `RoleEndpoint Role = "endpoints"`, the type of a composite literal (`var r
|
||||
* = &Registry{}`) or of a conversion (`var p = Role("pod")`) it is set to,
|
||||
* and for a constant written alone in a `const ( … )` group, what the spec
|
||||
* above it says, which Go repeats. Null for a value a package from outside
|
||||
* the project hands out (`var errX = errors.New(…)`). Undefined when the
|
||||
* spec says no type — a value a project function returns, an untyped
|
||||
* constant — or can't be read.
|
||||
*/
|
||||
function goPackageValueType(n: Node, context: ResolutionContext): GoPackageValueType | undefined {
|
||||
const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/);
|
||||
if (!lines) return undefined;
|
||||
for (let i = n.startLine - 1; i >= 0 && i > n.startLine - 200; i--) {
|
||||
const line = (lines[i] ?? '').replace(/\/\/.*$/, '');
|
||||
const own = i === n.startLine - 1;
|
||||
if (!own && /^\s*(?:$|\/\*|\*)/.test(line)) continue;
|
||||
// `var name T = v`, `const ( name T = v`, a group's `name, other = v`.
|
||||
const spec = /^\s*(?:(?:var|const)\s*(?:\(\s*)?)?([A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*)(.*)$/.exec(line);
|
||||
if (!spec || (own && !spec[1]!.split(/\s*,\s*/).includes(n.name))) return undefined;
|
||||
const rest = spec[2]!.trim();
|
||||
if (!rest) {
|
||||
// A constant written alone in its group repeats the spec above it.
|
||||
if (n.kind !== 'constant' || /^\s*(?:var|const)\b/.test(line)) return undefined;
|
||||
continue;
|
||||
}
|
||||
const eq = rest.search(/=(?!=)/);
|
||||
const written = (eq < 0 ? rest : rest.slice(0, eq)).trim();
|
||||
if (/[:"'`]/.test(written)) return undefined;
|
||||
if (written) return { written, file: n.filePath };
|
||||
return goValueType(rest.slice(eq + 1), n.filePath, context);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type a Go value gives a declaration that writes none: a composite
|
||||
* literal's (`&Registry{…}`, `pkg.T{…}`) or a conversion's (`Role("pod")`,
|
||||
* `pkg.T(x)`). Null for what a package from outside the project hands out
|
||||
* (`errors.New(…)`), and for an anonymous struct; undefined otherwise.
|
||||
*/
|
||||
function goValueType(value: string, filePath: string, context: ResolutionContext): GoPackageValueType | undefined {
|
||||
const m = /^\s*&?\s*(?:([A-Za-z_]\w*)\s*\.\s*)?([A-Za-z_]\w*)\s*(?:\[[^\]\n]*\]\s*)?([({])/.exec(value);
|
||||
if (!m) return undefined;
|
||||
const [, qualifier, name, open] = m;
|
||||
if (qualifier !== undefined && isGoOutsidePackage(qualifier, filePath, context)) return null;
|
||||
const written = qualifier !== undefined ? `${qualifier}.${name}` : name!;
|
||||
if (open === '{') return GO_TYPE_KEYWORDS.has(name!) ? null : { written, file: filePath };
|
||||
// A call is a conversion when it names a type of its package.
|
||||
const dir = qualifier !== undefined ? goImportPackageDir(qualifier, filePath, context) : goPackageDir(filePath);
|
||||
return dir && goPackageTypes(name!, dir, context).length > 0 ? { written, file: filePath } : undefined;
|
||||
}
|
||||
|
||||
const GO_PACKAGE_CLAUSES = new WeakMap<ResolutionContext, Map<string, string | null>>();
|
||||
|
||||
/** The name a Go file's `package` clause gives its package, or null. */
|
||||
function goPackageClause(filePath: string, context: ResolutionContext): string | null {
|
||||
let memo = GO_PACKAGE_CLAUSES.get(context);
|
||||
if (!memo) GO_PACKAGE_CLAUSES.set(context, (memo = new Map()));
|
||||
const hit = memo.get(filePath);
|
||||
if (hit !== undefined) return hit;
|
||||
const lines = context.getFileLines?.(filePath) ?? context.readFile(filePath)?.split(/\r?\n/) ?? [];
|
||||
let name: string | null = null;
|
||||
let comment = false;
|
||||
for (const line of lines.slice(0, 500)) {
|
||||
if (comment) {
|
||||
if (line.includes('*/')) comment = false;
|
||||
continue;
|
||||
}
|
||||
if (/^\s*\/\*/.test(line) && !line.includes('*/')) {
|
||||
comment = true;
|
||||
continue;
|
||||
}
|
||||
const m = /^\s*package\s+([A-Za-z_]\w*)/.exec(line);
|
||||
if (m) {
|
||||
name = m[1]!;
|
||||
break;
|
||||
}
|
||||
}
|
||||
memo.set(filePath, name);
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The names a Go file may bind from a type assertion — the first name before
|
||||
* each `:=` or `=` on a line that asserts a type — so that only calls through
|
||||
|
||||
Reference in New Issue
Block a user