mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-11-15 03:08:20 +08:00
fix(go): an unaliased import is also known by the name goimports assumes for its package (#2410)
An unaliased Go import was named by its path's last element, so the qualifier in yaml.Node (go.yaml.in/yaml/v3), klog.V (k8s.io/klog/v2) or sqlite3.Error (github.com/mattn/go-sqlite3) matched none of the file's imports. The reference then resolved by its bare name to whatever project symbol shared it. An unaliased import now also takes the name goimports' ImportPathToAssumedName gives its path (the last element that isn't a major version, without a go- prefix, cut at the first non-identifier character), unless another import of the file is bound to that name or a second import assumes it too. The last element stays registered: k8s.io/api/core/v1 really is package v1. The import scan now reads only the import section, without comments: the last word of a comment had become the next import's alias, and imports spelled in doc comments or string literals were read as real ones. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
73797bf1e4
commit
d8a7f868b3
@@ -1145,50 +1145,85 @@ function extractPythonImports(content: string): ImportMapping[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Go import mappings
|
||||
* Extract Go import mappings. An import is bound to the name written before
|
||||
* its path, or else to the name of the package it imports — which the path
|
||||
* alone doesn't settle: `k8s.io/api/core/v1` is package `v1`, while
|
||||
* `go.yaml.in/yaml/v3` and `gopkg.in/yaml.v3` are `yaml`, and
|
||||
* `github.com/mattn/go-sqlite3` is `sqlite3`. So an unaliased import keeps its
|
||||
* last path element and also takes the name goimports assumes for the path,
|
||||
* unless another of the file's imports is bound to that name.
|
||||
*/
|
||||
function extractGoImports(content: string): ImportMapping[] {
|
||||
const mappings: ImportMapping[] = [];
|
||||
const specs: Array<{ name: string | undefined; source: string }> = [];
|
||||
// Only the imports, which Go puts before any other declaration, and without
|
||||
// their comments: a word in one is no name — `"fmt" // for printing` used to
|
||||
// name the next import `printing`. A name is on its path's own line.
|
||||
const code = stripCommentsForRegex(content.slice(0, goImportsEnd(content)), 'go');
|
||||
|
||||
// import "path" or import alias "path"
|
||||
const singleImportRegex = /import\s+(?:(\w+)\s+)?["']([^"']+)["']/g;
|
||||
// import "path" or import name "path"
|
||||
const singleImportRegex = /\bimport\s+(?:([A-Za-z_]\w*)[ \t]*)?["']([^"']+)["']/g;
|
||||
let match;
|
||||
|
||||
while ((match = singleImportRegex.exec(content)) !== null) {
|
||||
const [, alias, source] = match;
|
||||
const packageName = source!.split('/').pop()!;
|
||||
mappings.push({
|
||||
localName: alias || packageName,
|
||||
exportedName: '*',
|
||||
source: source!,
|
||||
isDefault: false,
|
||||
isNamespace: true,
|
||||
});
|
||||
while ((match = singleImportRegex.exec(code)) !== null) {
|
||||
specs.push({ name: match[1], source: match[2]! });
|
||||
}
|
||||
|
||||
// import ( ... ) block
|
||||
const blockImportRegex = /import\s*\(\s*([^)]+)\s*\)/gs;
|
||||
while ((match = blockImportRegex.exec(content)) !== null) {
|
||||
const block = match[1]!;
|
||||
const lineRegex = /(?:(\w+)\s+)?["']([^"']+)["']/g;
|
||||
const blockImportRegex = /\bimport\s*\(\s*([^)]+)\s*\)/g;
|
||||
while ((match = blockImportRegex.exec(code)) !== null) {
|
||||
const lineRegex = /(?:([A-Za-z_]\w*|\.)[ \t]*)?["']([^"']+)["']/g;
|
||||
let lineMatch;
|
||||
|
||||
while ((lineMatch = lineRegex.exec(block)) !== null) {
|
||||
const [, alias, source] = lineMatch;
|
||||
const packageName = source!.split('/').pop()!;
|
||||
mappings.push({
|
||||
localName: alias || packageName,
|
||||
exportedName: '*',
|
||||
source: source!,
|
||||
isDefault: false,
|
||||
isNamespace: true,
|
||||
});
|
||||
while ((lineMatch = lineRegex.exec(match[1]!)) !== null) {
|
||||
specs.push({ name: lineMatch[1], source: lineMatch[2]! });
|
||||
}
|
||||
}
|
||||
|
||||
const mapping = (localName: string, source: string): ImportMapping =>
|
||||
({ localName, exportedName: '*', source, isDefault: false, isNamespace: true });
|
||||
// A dot import (`. "pkg"`) binds no name of its own; it stays listed under
|
||||
// its last path element.
|
||||
const mappings = specs.map(({ name, source }) =>
|
||||
mapping(name === undefined || name === '.' ? source.split('/').pop()! : name, source));
|
||||
const bound = new Set(mappings.map((m) => m.localName));
|
||||
const assumed = new Map<string, string[]>();
|
||||
for (const { name, source } of specs) {
|
||||
if (name !== undefined) continue;
|
||||
const assumedName = goAssumedPackageName(source);
|
||||
if (assumedName && !bound.has(assumedName)) assumed.set(assumedName, [...(assumed.get(assumedName) ?? []), source]);
|
||||
}
|
||||
// Two imports that assume one name can't both be that package.
|
||||
for (const [localName, sources] of assumed) {
|
||||
if (sources.length === 1) mappings.push(mapping(localName, sources[0]!));
|
||||
}
|
||||
return mappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a Go file's imports end: at its first other top-level declaration, a
|
||||
* line opening with `func`, `type`, `var` or `const` outside a block comment.
|
||||
*/
|
||||
function goImportsEnd(content: string): number {
|
||||
const decl = /^(?:func|type|var|const)\b/gm;
|
||||
for (let m; (m = decl.exec(content)) !== null;) {
|
||||
if (content.lastIndexOf('/*', m.index) <= content.lastIndexOf('*/', m.index)) return m.index;
|
||||
}
|
||||
return content.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* The package name goimports assumes for an import path
|
||||
* (ImportPathToAssumedName): its last element that isn't a major version
|
||||
* (`go.yaml.in/yaml/v3` → `yaml`), without a `go-` prefix
|
||||
* (`github.com/mattn/go-sqlite3` → `sqlite3`), cut where an identifier can't
|
||||
* go on (`gopkg.in/yaml.v3` → `yaml`).
|
||||
*/
|
||||
function goAssumedPackageName(importPath: string): string {
|
||||
const elements = importPath.split('/');
|
||||
let base = elements[elements.length - 1]!;
|
||||
if (elements.length > 1 && /^v\d+$/.test(base)) base = elements[elements.length - 2]!;
|
||||
if (base.startsWith('go-')) base = base.slice(3);
|
||||
return /^[\p{L}\p{Nd}_]*/u.exec(base)![0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract Java / Kotlin import mappings.
|
||||
*
|
||||
|
||||
@@ -3125,10 +3125,10 @@ export class ReferenceResolver {
|
||||
result = { ...result, targetNodeId: type.id };
|
||||
}
|
||||
if (isBoundToOutOfRepoImport(ref, this.context)) return null;
|
||||
// A Go type embedded through a package (`yaml.Node`) is that package's.
|
||||
// When the package is none of the file's imports as indexed, a type found
|
||||
// by its bare name is some other package's namesake: prometheus's
|
||||
// `RuleGroupNode` embeds yaml's `Node`, not discovery/kubernetes's.
|
||||
// A Go type embedded through a package (`clientv3.KV`) is that package's.
|
||||
// When the package is none of the file's imports as indexed (clientv3
|
||||
// imported unaliased as `go.etcd.io/etcd/client/v3`), a type found by its
|
||||
// bare name is some other package's namesake.
|
||||
if (ref.language === 'go' && isGoUnknownQualified(ref, this.context)) return null;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1927,9 +1927,10 @@ export function goRefQualifier(ref: UnresolvedRef, context: ResolutionContext):
|
||||
|
||||
/**
|
||||
* Whether a Go reference is written through a package that is none of its
|
||||
* file's imports as the index knows them: `yaml` in `yaml.Node` under an
|
||||
* unaliased `import "go.yaml.in/yaml/v3"`, which is recorded under its last
|
||||
* path element, `v3`. Which package that is cannot be told from here.
|
||||
* file's imports as the index knows them: `clientv3` in `clientv3.KV` under an
|
||||
* unaliased `import "go.etcd.io/etcd/client/v3"`, a package named neither by
|
||||
* its path's last element (`v3`) nor by the name goimports assumes for it
|
||||
* (`client`). Which package that is cannot be told from here.
|
||||
*/
|
||||
export function isGoUnknownQualified(ref: UnresolvedRef, context: ResolutionContext): boolean {
|
||||
const { written, imported } = goRefQualification(ref, context);
|
||||
|
||||
Reference in New Issue
Block a user