Files
aego/tools/depcheck/graph.go
T

172 lines
4.2 KiB
Go

package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
type pkgInfo struct {
path string // module-relative
dir string
files []string
testFiles []string
imports []string // module-relative, own module only
testImps []string
external []string // non-module, non-std imports
}
type graph struct {
variant string
pkgs map[string]*pkgInfo
order []string
}
type variantSpec struct {
name string
tags string
env []string
patterns []string
}
const listFields = "ImportPath,Dir,GoFiles,CgoFiles,TestGoFiles,XTestGoFiles,Imports,TestImports,XTestImports,Standard,Error,DepsErrors"
func loadGraph(root, module string, v variantSpec) (*graph, error) {
args := []string{"list", "-e", "-deps", "-json=" + listFields}
if v.tags != "" {
args = append(args, "-tags", v.tags)
}
args = append(args, v.patterns...)
cmd := exec.Command("go", args...)
cmd.Dir = root
cmd.Env = append(append(os.Environ(), "GOWORK=off"), v.env...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("%s: go list: %w: %s", v.name, err, stderr.String())
}
g := &graph{variant: v.name, pkgs: map[string]*pkgInfo{}}
var errs []string
dec := json.NewDecoder(bytes.NewReader(out))
for dec.More() {
var p struct {
ImportPath string
Dir string
GoFiles, CgoFiles, TestGoFiles, XTestGoFiles []string
Imports, TestImports, XTestImports []string
Standard bool
Error *struct{ Err string }
DepsErrors []struct{ Err string }
}
if err := dec.Decode(&p); err != nil {
return nil, err
}
if p.Standard {
continue
}
local, ok := relPkg(module, p.ImportPath)
if p.Error != nil {
errs = append(errs, p.ImportPath+": "+p.Error.Err)
}
for _, d := range p.DepsErrors {
errs = append(errs, p.ImportPath+": "+d.Err)
}
if !ok {
continue
}
info := &pkgInfo{
path: local,
dir: p.Dir,
files: append(p.GoFiles, p.CgoFiles...),
testFiles: append(p.TestGoFiles, p.XTestGoFiles...),
}
for _, imp := range p.Imports {
if l, ok := relPkg(module, imp); ok {
info.imports = append(info.imports, l)
} else if imp != "C" && !isStdlib(imp) {
info.external = append(info.external, imp)
}
}
for _, imp := range append(p.TestImports, p.XTestImports...) {
if l, ok := relPkg(module, imp); ok && l != local {
info.testImps = append(info.testImps, l)
}
}
g.pkgs[local] = info
g.order = append(g.order, local)
}
if len(errs) > 0 {
return nil, fmt.Errorf("%s: go list reported errors:\n %s", v.name, strings.Join(errs, "\n "))
}
return g, nil
}
// path returns an import chain from one of roots to target, or nil.
func (g *graph) path(roots []string, target string) []string {
parent := map[string]string{}
queue := []string{}
for _, r := range roots {
if _, ok := g.pkgs[r]; ok {
parent[r] = ""
queue = append(queue, r)
}
}
for len(queue) > 0 {
p := queue[0]
queue = queue[1:]
if p == target {
var chain []string
for q := p; ; q = parent[q] {
chain = append([]string{q}, chain...)
if parent[q] == "" {
return chain
}
}
}
for _, imp := range g.pkgs[p].imports {
if _, seen := parent[imp]; !seen {
if _, ok := g.pkgs[imp]; ok {
parent[imp] = p
queue = append(queue, imp)
}
}
}
}
return nil
}
func modulePath(root string) (string, error) {
data, err := os.ReadFile(filepath.Join(root, "go.mod"))
if err != nil {
return "", err
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line)
if rest, ok := strings.CutPrefix(line, "module "); ok {
return strings.Trim(strings.TrimSpace(rest), `"`), nil
}
}
return "", errors.New("module directive not found in go.mod")
}
func relPkg(module, path string) (string, bool) {
if path == module {
return "", true
}
if rest, ok := strings.CutPrefix(path, module+"/"); ok {
return rest, true
}
return "", false
}
func isStdlib(path string) bool {
first, _, _ := strings.Cut(path, "/")
return !strings.Contains(first, ".")
}