a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
641 lines
20 KiB
Go
641 lines
20 KiB
Go
package goanalysis
|
|
|
|
import (
|
|
"go/types"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/packages"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/semantic"
|
|
)
|
|
|
|
// stdlibModuleID is the synthetic KindModule node ID used to attribute Go
|
|
// standard-library symbols. The standard library has no go.mod entry, so
|
|
// we materialise a single shared module node so dependency queries
|
|
// ("which modules does pkg X reach into?") return a stdlib bucket
|
|
// alongside real module-cache modules.
|
|
const stdlibModuleID = "module::go:stdlib"
|
|
|
|
// modulePathStdlib is the canonical "path" for the stdlib module node. Kept
|
|
// in sync with the existing internal/modules naming so any consumer that
|
|
// groups KindModule nodes by Meta["path"] can find stdlib alongside real
|
|
// go.mod entries.
|
|
const modulePathStdlib = "stdlib"
|
|
|
|
// externalsAttribution holds bookkeeping for a single Enrich() pass. It
|
|
// owns:
|
|
//
|
|
// - pkgByPath: every transitively loaded *packages.Package, indexed by
|
|
// import path. Top-level packages.Load returns only the repo's own
|
|
// packages; we walk pkg.Imports recursively so external symbol
|
|
// resolution can find the owning module for stdlib / module-cache
|
|
// packages too.
|
|
// - moduleByPath: import path → KindModule node ID. Cached so the
|
|
// stdlib (and each dep module) materialises at most once per pass.
|
|
// - extByObj: types.Object → external node ID. Caches lookups across
|
|
// multiple Uses of the same external symbol.
|
|
//
|
|
// Statistics counters surface back through ExternalsResult so the caller
|
|
// can report nodes/edges added.
|
|
type externalsAttribution struct {
|
|
g graph.Store
|
|
pkgByPath map[string]*packages.Package
|
|
moduleByPath map[string]string
|
|
extByObj map[types.Object]string
|
|
provider string
|
|
|
|
// repoPrefix is the owning repo's prefix, used to namespace stub
|
|
// IDs (graph.StubID). Empty when the caller doesn't supply one
|
|
// — in that case stub IDs are emitted in the legacy un-prefixed
|
|
// form, which graph.IsStdlibStub / friends still recognise.
|
|
repoPrefix string
|
|
|
|
nodesAdded int
|
|
edgesAdded int
|
|
edgesUpgraded int
|
|
modulesLinked int
|
|
stdlibCreated bool
|
|
missingPkgInfo int
|
|
}
|
|
|
|
// newExternalsAttribution prepares externalsAttribution from the loaded
|
|
// roots. Walking pkg.Imports collects every dep — stdlib and module-cache
|
|
// alike — so resolveSymbol can find the owning *packages.Package for an
|
|
// arbitrary types.Object.
|
|
func newExternalsAttribution(g graph.Store, roots []*packages.Package, provider string) *externalsAttribution {
|
|
pkgByPath := make(map[string]*packages.Package)
|
|
var visit func(p *packages.Package)
|
|
visit = func(p *packages.Package) {
|
|
if p == nil || p.PkgPath == "" {
|
|
return
|
|
}
|
|
if _, seen := pkgByPath[p.PkgPath]; seen {
|
|
return
|
|
}
|
|
pkgByPath[p.PkgPath] = p
|
|
for _, imp := range p.Imports {
|
|
visit(imp)
|
|
}
|
|
}
|
|
for _, r := range roots {
|
|
visit(r)
|
|
}
|
|
return &externalsAttribution{
|
|
g: g,
|
|
pkgByPath: pkgByPath,
|
|
moduleByPath: make(map[string]string),
|
|
extByObj: make(map[types.Object]string),
|
|
provider: provider,
|
|
repoPrefix: deriveRepoPrefix(g, roots),
|
|
}
|
|
}
|
|
|
|
// deriveRepoPrefix peeks at the first source file across the
|
|
// enrichment roots and reads its RepoPrefix from the graph.
|
|
// All files belonging to a single semantic.Provider.Enrich call
|
|
// share one repo, so a single sample suffices. Returns "" when no
|
|
// matching file node is found — stubs then fall back to the
|
|
// legacy un-prefixed form, which graph.IsStdlibStub still accepts.
|
|
func deriveRepoPrefix(g graph.Store, roots []*packages.Package) string {
|
|
for _, r := range roots {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
for _, f := range r.GoFiles {
|
|
if nodes := g.GetFileNodes(f); len(nodes) > 0 {
|
|
for _, n := range nodes {
|
|
if n != nil && n.RepoPrefix != "" {
|
|
return n.RepoPrefix
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// resolveSymbol returns the graph node ID for an external go/types object,
|
|
// creating it (and the owning KindModule node, if not already present)
|
|
// on first sight. Returns "" when the object is unsuitable for
|
|
// externalisation: builtins / universe-scope (no Pkg), unsupported obj
|
|
// kinds, or a missing *packages.Package entry (rare — would mean a Use
|
|
// pointed at a package the loader didn't see).
|
|
//
|
|
// External symbols become first-class graph nodes so the call graph can
|
|
// reach into stdlib / module-cache without leaving stub-string targets
|
|
// behind. Each external symbol gains an EdgeDependsOnModule to its
|
|
// owning KindModule — i.e. stdlib/dep attribution.
|
|
func (e *externalsAttribution) resolveSymbol(obj types.Object) string {
|
|
if obj == nil || obj.Pkg() == nil {
|
|
return ""
|
|
}
|
|
if id, ok := e.extByObj[obj]; ok {
|
|
return id
|
|
}
|
|
|
|
importPath := obj.Pkg().Path()
|
|
if importPath == "" {
|
|
return ""
|
|
}
|
|
pkg, ok := e.pkgByPath[importPath]
|
|
if !ok {
|
|
// Loader didn't see this package — happens when packages.Load
|
|
// returns errors and the user-facing pkg list is partial. Bail
|
|
// rather than synthesize a half-formed node.
|
|
e.missingPkgInfo++
|
|
return ""
|
|
}
|
|
moduleID := e.ensureModuleNode(pkg)
|
|
if moduleID == "" {
|
|
// pkg belongs to the indexed repo (Main module). Caller should
|
|
// have routed obj through objToNode before falling back to
|
|
// externals — return "" so the caller drops the use.
|
|
return ""
|
|
}
|
|
|
|
kind := externalNodeKind(obj)
|
|
if kind == "" {
|
|
return ""
|
|
}
|
|
nodeID := externalNodeID(importPath, obj)
|
|
if nodeID == "" {
|
|
return ""
|
|
}
|
|
|
|
if existing := e.g.GetNode(nodeID); existing == nil {
|
|
e.g.AddNode(buildExternalNode(nodeID, kind, importPath, moduleID, pkg, obj, e.provider))
|
|
e.nodesAdded++
|
|
|
|
// Attribute the symbol to its module via EdgeDependsOnModule. The
|
|
// schema's existing convention is "file/package/import →
|
|
// KindModule"; an external symbol is morally equivalent — it's
|
|
// a first-class entity that depends on the module providing it.
|
|
e.g.AddEdge(&graph.Edge{
|
|
From: nodeID,
|
|
To: moduleID,
|
|
Kind: graph.EdgeDependsOnModule,
|
|
FilePath: externalFilePath(importPath),
|
|
Line: 0,
|
|
Confidence: 1.0,
|
|
ConfidenceLabel: "EXTRACTED",
|
|
Origin: graph.OriginLSPResolved,
|
|
Meta: map[string]any{
|
|
"semantic_source": e.provider,
|
|
},
|
|
})
|
|
e.edgesAdded++
|
|
e.modulesLinked++
|
|
}
|
|
e.extByObj[obj] = nodeID
|
|
return nodeID
|
|
}
|
|
|
|
// claimAndUpgradeStub looks for an existing edge from caller to one of the
|
|
// resolver's stub targets for this external symbol (stdlib::, dep::, or
|
|
// unresolved::extern::) and rewrites its To to point at the new external
|
|
// node. Returns the rewritten edge for the caller to confirm, or nil
|
|
// when no stub is found.
|
|
//
|
|
// Two passes:
|
|
//
|
|
// 1. Exact stub-string lookup. The parser+resolver shape for direct
|
|
// package calls like `fmt.Println(...)` lands as `stdlib::fmt::Println`
|
|
// after the resolver runs (or `unresolved::extern::fmt::Println` if
|
|
// resolution didn't happen yet). We replace the To with the real
|
|
// external node ID.
|
|
// 2. Fuzzy line-and-name match. Method calls on external types (e.g.
|
|
// `os.Stdout.Write(...)`) land as `unresolved::*.Write` because the
|
|
// parser doesn't know `os.Stdout` resolves to the os.File receiver.
|
|
// The fuzzy pass scans the caller's outgoing edges at the same line
|
|
// and matches by trailing-name, which is enough to correctly claim
|
|
// the stub without bringing in line-unrelated false positives.
|
|
//
|
|
// Why this matters: previously the resolver wrote stub-string targets like
|
|
// "stdlib::fmt::Println" that no node holds. Once goanalysis materialises
|
|
// the real ext::go:fmt::Println node, leaving the stub edge in place
|
|
// would double-count the call (one stub, one real). ReindexEdge migrates
|
|
// the byTo bucket so find_usages on the new node returns the correct
|
|
// caller and the stub bucket drains.
|
|
func (e *externalsAttribution) claimAndUpgradeStub(callerID string, importPath string, obj types.Object, newTarget string, line int) *graph.Edge {
|
|
if edge := e.claimByExactStub(callerID, importPath, obj, newTarget); edge != nil {
|
|
return edge
|
|
}
|
|
if edge := e.claimByLineAndName(callerID, obj, newTarget, line); edge != nil {
|
|
return edge
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// claimByExactStub handles the canonical resolver-shaped targets. Pulled
|
|
// out so the fuzzy pass can layer on top.
|
|
func (e *externalsAttribution) claimByExactStub(callerID string, importPath string, obj types.Object, newTarget string) *graph.Edge {
|
|
candidates := stubEdgeTargets(e.repoPrefix, importPath, obj)
|
|
for _, target := range candidates {
|
|
edge := semantic.FindEdgeByTarget(e.g, callerID, target)
|
|
if edge == nil {
|
|
continue
|
|
}
|
|
oldTo := edge.To
|
|
edge.To = newTarget
|
|
e.g.ReindexEdge(edge, oldTo)
|
|
semantic.ConfirmEdge(edge, e.provider)
|
|
e.edgesUpgraded++
|
|
return edge
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// claimByLineAndName scans the caller's outgoing edges at line `line` for
|
|
// any edge whose target is still a stub-string (`unresolved::`, `external::`,
|
|
// `stdlib::`, `dep::`) and whose trailing-name matches obj.Name(). Used
|
|
// for method calls on external types where the parser's `unresolved::*.M`
|
|
// shape doesn't carry the import path.
|
|
//
|
|
// Conservative — only matches stub targets so we never overwrite a
|
|
// resolver-confirmed real edge — and only when both line and trailing
|
|
// name match, which together pin the use-site uniquely.
|
|
func (e *externalsAttribution) claimByLineAndName(callerID string, obj types.Object, newTarget string, line int) *graph.Edge {
|
|
if line <= 0 {
|
|
return nil
|
|
}
|
|
name := obj.Name()
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
expected := wantedEdgeKind(obj)
|
|
for _, edge := range e.g.GetOutEdges(callerID) {
|
|
if edge.Line != line {
|
|
continue
|
|
}
|
|
if expected != "" && edge.Kind != expected {
|
|
continue
|
|
}
|
|
if !isStubTarget(edge.To) {
|
|
continue
|
|
}
|
|
if !stubTargetTrailingNameMatches(edge.To, name) {
|
|
continue
|
|
}
|
|
oldTo := edge.To
|
|
edge.To = newTarget
|
|
e.g.ReindexEdge(edge, oldTo)
|
|
semantic.ConfirmEdge(edge, e.provider)
|
|
e.edgesUpgraded++
|
|
return edge
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// wantedEdgeKind returns the EdgeKind goanalysis would emit for obj, used
|
|
// to scope the fuzzy claim so we don't accidentally rewrite an unrelated
|
|
// edge with a different semantic.
|
|
func wantedEdgeKind(obj types.Object) graph.EdgeKind {
|
|
if obj == nil {
|
|
return ""
|
|
}
|
|
switch obj.(type) {
|
|
case *types.Func:
|
|
return graph.EdgeCalls
|
|
case *types.TypeName, *types.Var, *types.Const:
|
|
return graph.EdgeReferences
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// isStubTarget reports whether a target ID is one of the bookkeeping
|
|
// strings the resolver writes for unresolved or external lookups.
|
|
func isStubTarget(to string) bool {
|
|
switch {
|
|
case graph.IsUnresolvedTarget(to),
|
|
strings.HasPrefix(to, "external::"),
|
|
graph.IsStdlibStub(to),
|
|
strings.HasPrefix(to, "dep::"):
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// stubTargetTrailingNameMatches reports whether the trailing name of a
|
|
// stub target (everything after the final `::` or after `*.`) equals
|
|
// `name`. The encodings include:
|
|
//
|
|
// unresolved::FooBar → trailing FooBar
|
|
// unresolved::*.Method → trailing Method
|
|
// unresolved::extern::pkg::Sym → trailing Sym
|
|
// stdlib::pkg::Sym → trailing Sym
|
|
// dep::pkg::Sym → trailing Sym
|
|
// external::pkg → trailing pkg (file-level imports)
|
|
func stubTargetTrailingNameMatches(to, name string) bool {
|
|
trailing := to
|
|
if idx := strings.LastIndex(trailing, "::"); idx >= 0 {
|
|
trailing = trailing[idx+2:]
|
|
}
|
|
trailing = strings.TrimPrefix(trailing, "*.")
|
|
return trailing == name
|
|
}
|
|
|
|
// ensureModuleNode finds (or creates) the KindModule node for pkg's owning
|
|
// module. Returns the module node ID, or "" when pkg belongs to the
|
|
// indexed repo's main module (no externalisation needed).
|
|
//
|
|
// The stdlib (pkg.Module == nil for stdlib) shares one synthetic
|
|
// "module::go:stdlib" node so callers can group stdlib references under
|
|
// a single edge target. Module-cache packages reuse any existing
|
|
// KindModule node materialised from go.mod by internal/modules and
|
|
// otherwise fall back to creating one — go/types is the source of
|
|
// truth when go.mod parsing missed the dep (e.g. tooling/test fixtures).
|
|
func (e *externalsAttribution) ensureModuleNode(pkg *packages.Package) string {
|
|
if pkg == nil {
|
|
return ""
|
|
}
|
|
importPath := pkg.PkgPath
|
|
if id, ok := e.moduleByPath[importPath]; ok {
|
|
return id
|
|
}
|
|
|
|
if pkg.Module == nil {
|
|
// No module info → assume Go stdlib. The stdlib has no go.mod
|
|
// entry; one shared module node covers every stdlib package.
|
|
if !e.stdlibCreated {
|
|
if existing := e.g.GetNode(stdlibModuleID); existing == nil {
|
|
e.g.AddNode(&graph.Node{
|
|
ID: stdlibModuleID,
|
|
Kind: graph.KindModule,
|
|
Name: "stdlib",
|
|
FilePath: externalFilePath("std"),
|
|
Language: "go",
|
|
Meta: map[string]any{
|
|
"ecosystem": "go",
|
|
"path": modulePathStdlib,
|
|
"version": "",
|
|
"module_kind": "stdlib",
|
|
"semantic_source": e.provider,
|
|
},
|
|
})
|
|
e.nodesAdded++
|
|
}
|
|
e.stdlibCreated = true
|
|
}
|
|
e.moduleByPath[importPath] = stdlibModuleID
|
|
return stdlibModuleID
|
|
}
|
|
|
|
if pkg.Module.Main {
|
|
// The indexed repo itself — caller should treat this obj as
|
|
// internal and look it up in objToNode instead.
|
|
e.moduleByPath[importPath] = ""
|
|
return ""
|
|
}
|
|
|
|
mod := pkg.Module
|
|
if mod.Replace != nil {
|
|
mod = mod.Replace
|
|
}
|
|
moduleID := goModuleNodeID(mod.Path, mod.Version)
|
|
if existing := e.g.GetNode(moduleID); existing == nil {
|
|
meta := map[string]any{
|
|
"ecosystem": "go",
|
|
"path": mod.Path,
|
|
"version": mod.Version,
|
|
"module_kind": "module_cache",
|
|
"indirect": mod.Indirect,
|
|
"semantic_source": e.provider,
|
|
}
|
|
if pkg.Module.Replace != nil {
|
|
meta["replace"] = mod.Path + "@" + mod.Version
|
|
meta["replaced_path"] = pkg.Module.Path
|
|
}
|
|
e.g.AddNode(&graph.Node{
|
|
ID: moduleID,
|
|
Kind: graph.KindModule,
|
|
Name: shortModulePath(mod.Path),
|
|
FilePath: externalFilePath(mod.Path),
|
|
Language: "go",
|
|
Meta: meta,
|
|
})
|
|
e.nodesAdded++
|
|
}
|
|
e.moduleByPath[importPath] = moduleID
|
|
return moduleID
|
|
}
|
|
|
|
// stubEdgeTargets enumerates every stub-string the resolver might have
|
|
// written for an external obj. Order matches resolver precedence:
|
|
// stdlib::/dep:: are produced post-resolve, unresolved::extern:: is the
|
|
// raw form when resolveExtern wasn't run.
|
|
//
|
|
// repoPrefix namespaces the stdlib stub form per-repo so two repos
|
|
// pinned to different Go SDK versions don't collide on a single
|
|
// `stdlib::fmt::Errorf` node. An empty repoPrefix yields the legacy
|
|
// un-prefixed form, which the resolver still emits today.
|
|
func stubEdgeTargets(repoPrefix, importPath string, obj types.Object) []string {
|
|
if obj == nil {
|
|
return nil
|
|
}
|
|
name := obj.Name()
|
|
if name == "" {
|
|
return nil
|
|
}
|
|
return []string{
|
|
graph.StubID(repoPrefix, graph.StubKindStdlib, importPath, name),
|
|
"dep::" + importPath + "::" + name,
|
|
"unresolved::extern::" + importPath + "::" + name,
|
|
}
|
|
}
|
|
|
|
// buildExternalNode constructs a graph node for an external symbol. The
|
|
// FilePath is a synthetic "external::go:<importPath>" string so byFile
|
|
// lookups don't pollute real source-file buckets.
|
|
func buildExternalNode(nodeID string, kind graph.NodeKind, importPath, moduleID string, pkg *packages.Package, obj types.Object, provider string) *graph.Node {
|
|
moduleKind, version := classifyPackage(pkg)
|
|
meta := map[string]any{
|
|
"external": true,
|
|
"import_path": importPath,
|
|
"module_path": modulePathOf(pkg),
|
|
"module_kind": moduleKind,
|
|
"module_id": moduleID,
|
|
"semantic_source": provider,
|
|
}
|
|
if version != "" {
|
|
meta["version"] = version
|
|
}
|
|
if sig := types.ObjectString(obj, nil); sig != "" {
|
|
meta["signature"] = sig
|
|
}
|
|
if typeStr := types.TypeString(obj.Type(), nil); typeStr != "" && typeStr != "invalid type" {
|
|
meta["semantic_type"] = typeStr
|
|
}
|
|
if recv := receiverTypeName(obj); recv != "" {
|
|
meta["receiver"] = recv
|
|
}
|
|
qualName := importPath + "." + obj.Name()
|
|
if recv := receiverTypeName(obj); recv != "" {
|
|
qualName = importPath + "." + recv + "." + obj.Name()
|
|
}
|
|
return &graph.Node{
|
|
ID: nodeID,
|
|
Kind: kind,
|
|
Name: obj.Name(),
|
|
QualName: qualName,
|
|
FilePath: externalFilePath(importPath),
|
|
Language: "go",
|
|
Meta: meta,
|
|
}
|
|
}
|
|
|
|
// classifyPackage labels a loaded package as stdlib / module_cache / main.
|
|
// Returns (kind, version) — version is empty for stdlib and main.
|
|
func classifyPackage(pkg *packages.Package) (string, string) {
|
|
if pkg == nil {
|
|
return "", ""
|
|
}
|
|
if pkg.Module == nil {
|
|
return "stdlib", ""
|
|
}
|
|
if pkg.Module.Main {
|
|
return "main", ""
|
|
}
|
|
mod := pkg.Module
|
|
if mod.Replace != nil {
|
|
mod = mod.Replace
|
|
}
|
|
return "module_cache", mod.Version
|
|
}
|
|
|
|
// modulePathOf returns the owning module's path for pkg. Stdlib packages
|
|
// fall back to the package import path so callers can group by package
|
|
// (the stdlib has no module path proper); module-cache packages return
|
|
// the module path which may be a prefix of the import path.
|
|
func modulePathOf(pkg *packages.Package) string {
|
|
if pkg == nil {
|
|
return ""
|
|
}
|
|
if pkg.Module == nil {
|
|
return pkg.PkgPath
|
|
}
|
|
mod := pkg.Module
|
|
if mod.Replace != nil {
|
|
mod = mod.Replace
|
|
}
|
|
return mod.Path
|
|
}
|
|
|
|
// receiverTypeName returns the bare type name of obj's receiver when obj
|
|
// is a method. Strips pointer wrappers — Go forbids both `func (T) M()`
|
|
// and `func (*T) M()` on the same type, so the bare name uniquely
|
|
// identifies the method's owner. Returns "" for non-methods.
|
|
func receiverTypeName(obj types.Object) string {
|
|
fn, ok := obj.(*types.Func)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
sig, ok := fn.Type().(*types.Signature)
|
|
if !ok || sig.Recv() == nil {
|
|
return ""
|
|
}
|
|
t := sig.Recv().Type()
|
|
if ptr, ok := t.(*types.Pointer); ok {
|
|
t = ptr.Elem()
|
|
}
|
|
if named, ok := t.(*types.Named); ok {
|
|
return named.Obj().Name()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// externalNodeID returns the canonical ID for an external symbol node.
|
|
// Methods are disambiguated by receiver type so `os.File.Write` and a
|
|
// hypothetical `os.Foo.Write` would land on distinct nodes; pure
|
|
// functions / vars / consts / types use the package-qualified short
|
|
// name. Returns "" when name is empty.
|
|
func externalNodeID(importPath string, obj types.Object) string {
|
|
name := obj.Name()
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
if recv := receiverTypeName(obj); recv != "" {
|
|
return "ext::go:" + importPath + "::" + recv + "." + name
|
|
}
|
|
return "ext::go:" + importPath + "::" + name
|
|
}
|
|
|
|
// externalNodeKind classifies a types.Object into the matching graph
|
|
// NodeKind. Funcs with a receiver become methods; type names with an
|
|
// interface underlying type become interfaces. Unsupported obj kinds
|
|
// (PkgName, Label, Builtin, Nil) return "" so resolveSymbol drops them.
|
|
func externalNodeKind(obj types.Object) graph.NodeKind {
|
|
switch t := obj.(type) {
|
|
case *types.Func:
|
|
if sig, ok := t.Type().(*types.Signature); ok && sig.Recv() != nil {
|
|
return graph.KindMethod
|
|
}
|
|
return graph.KindFunction
|
|
case *types.TypeName:
|
|
if t.Type() == nil {
|
|
return graph.KindType
|
|
}
|
|
if _, isIface := t.Type().Underlying().(*types.Interface); isIface {
|
|
return graph.KindInterface
|
|
}
|
|
return graph.KindType
|
|
case *types.Var:
|
|
return graph.KindVariable
|
|
case *types.Const:
|
|
return graph.KindConstant
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// externalFilePath returns the synthetic file-path used for external
|
|
// nodes. Kept consistent so byFile lookups don't pollute real file
|
|
// buckets.
|
|
func externalFilePath(importPath string) string {
|
|
if importPath == "" {
|
|
return "external::go"
|
|
}
|
|
return "external::go:" + importPath
|
|
}
|
|
|
|
// goModuleNodeID returns the canonical KindModule node ID for a Go
|
|
// module. Mirrors internal/modules/scanner.go::ModuleNodeID without
|
|
// importing scanner — that import would create graph→modules→graph
|
|
// cycle when scanner already imports graph.
|
|
func goModuleNodeID(path, version string) string {
|
|
id := "module::go:" + path
|
|
if version != "" {
|
|
id += "@" + version
|
|
}
|
|
return id
|
|
}
|
|
|
|
// shortModulePath returns the last meaningful segment of a module path,
|
|
// stripping the /vN major-version suffix so `github.com/foo/bar/v2`
|
|
// surfaces as `bar` not `v2`. Mirrors internal/modules/scanner.go::shortName.
|
|
func shortModulePath(path string) string {
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
parts := strings.Split(path, "/")
|
|
last := parts[len(parts)-1]
|
|
if len(parts) >= 2 && isMajorVersion(last) {
|
|
last = parts[len(parts)-2]
|
|
}
|
|
return last
|
|
}
|
|
|
|
// isMajorVersion matches "v2", "v3", "v10" etc. — the Go-modules SemVer
|
|
// major-version suffix that lives at the end of a module path.
|
|
func isMajorVersion(s string) bool {
|
|
if len(s) < 2 || s[0] != 'v' {
|
|
return false
|
|
}
|
|
for i := 1; i < len(s); i++ {
|
|
if s[i] < '0' || s[i] > '9' {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|