Files
mesh-catalog/checks/requiredmodules/main.go
T
jschoubben 62fe1cc36d
mesh/check failed (4): unit: git.novox.be/novox/m…; and 3 more | 62fe1cc36d30 on f2b332de73d6 | judged by the candidate's mesh-build-go at e8757a23634d
mesh/delivery-group group feat/registry-milestone rejected: composed together: the checker's verdict at the members' heads (8a80e8e86858): unit: git.novox.be/n…
mesh/delivery closed: a newer head of the same pull request
mesh-build-go requires the checker client it imports; a check refuses any module importing a mesh module its go.mod does not require (issue 1921, issue 1966)
Quick fix: add the one require to mesh-build-go's go.mod (a minute). Thorough fix, chosen because the kind was met twice (a
local go.work resolving what go.mod never names) and hides itself from every local build: checks/requiredmodules reads every
go.mod and Go source (tests and tagged files included), finds the owning module by the longest module line among the
repositories beside, and fails in go test and the repository check before any push. The scan over the seven group heads
found this one instance.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-07 04:23:47 +01:00

205 lines
5.6 KiB
Go

// Command requiredmodules refuses a Go module that imports a package of a mesh module its go.mod does not require
// (issue 1921, issue 1966; the second time a local workspace hid it: the candidate's build resolves only what go.mod says).
//
// A developer's go.work resolves every sibling module, so a source can import a package whose module the go.mod never
// names and still build; the build seat builds against the go.mod alone and then fails with "finding module for
// package". This check reads every go.mod under the roots it reports and every Go source beside it (test files and files
// behind a build tag included, testdata, vendor and node_modules left out) and, for each import under
// git.novox.be/novox/, finds the module that owns the package (the longest `module` line, among the modules of every
// root it is given, that is a prefix of the import) and demands that module to be the go.mod's own or one it requires.
// Where no known module owns the import, any own or required module that is a prefix of it satisfies it.
//
// go run . <root> [<root> ...]
//
// The first root is reported; the others (the repositories cloned beside, MESH_CHECK_BESIDE) only say which module owns
// which path. With -all every root is reported. Exits 1 on a finding.
package main
import (
"fmt"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
const prefix = "git.novox.be/novox/"
// Mod is a go.mod: where it is, its module path and the modules it requires.
type Mod struct {
Dir string
Path string
Requires map[string]bool
}
// Modules reads every go.mod under root.
func Modules(root string) ([]Mod, error) {
var out []Mod
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case "testdata", "vendor", "node_modules", ".git":
return fs.SkipDir
}
return nil
}
if d.Name() != "go.mod" {
return nil
}
raw, err := os.ReadFile(p)
if err != nil {
return err
}
m := Mod{Dir: filepath.Dir(p), Requires: map[string]bool{}}
in := false
for _, line := range strings.Split(string(raw), "\n") {
line = strings.TrimSpace(line)
if i := strings.Index(line, "//"); i >= 0 {
line = strings.TrimSpace(line[:i])
}
f := strings.Fields(line)
switch {
case len(f) == 2 && f[0] == "module":
m.Path = strings.Trim(f[1], `"`)
case len(f) >= 1 && f[0] == "require" && len(f) >= 2 && f[1] == "(":
in = true
case line == ")":
in = false
case len(f) >= 3 && f[0] == "require":
m.Requires[f[1]] = true
case in && len(f) >= 2:
m.Requires[f[0]] = true
}
}
if m.Path == "" {
return fmt.Errorf("%s names no module", p)
}
out = append(out, m)
return nil
})
return out, err
}
func within(path, module string) bool { return path == module || strings.HasPrefix(path, module+"/") }
// Findings reports, for every go.mod under the report roots, each imported package whose module the go.mod neither is nor
// requires; known are the other roots that say which module owns a path.
func Findings(report, known []string) ([]string, error) {
var owners []string
var reported []Mod
for _, r := range report {
ms, err := Modules(r)
if err != nil {
return nil, err
}
reported = append(reported, ms...)
}
for _, r := range append(append([]string{}, report...), known...) {
ms, err := Modules(r)
if err != nil {
return nil, err
}
for _, m := range ms {
owners = append(owners, m.Path)
}
}
var out []string
for _, m := range reported {
seen := map[string]bool{}
err := filepath.WalkDir(m.Dir, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
switch d.Name() {
case "testdata", "vendor", "node_modules", ".git":
return fs.SkipDir
}
if p != m.Dir {
if _, err := os.Stat(filepath.Join(p, "go.mod")); err == nil {
return fs.SkipDir // a nested module is judged by its own go.mod
}
}
return nil
}
if !strings.HasSuffix(p, ".go") {
return nil
}
f, err := parser.ParseFile(token.NewFileSet(), p, nil, parser.ImportsOnly)
if err != nil {
return fmt.Errorf("%s: %w", p, err)
}
for _, im := range f.Imports {
path, _ := strconv.Unquote(im.Path.Value)
if !strings.HasPrefix(path, prefix) || seen[path] {
continue
}
owner := ""
for _, o := range owners {
if within(path, o) && len(o) > len(owner) {
owner = o
}
}
ok := false
if owner != "" {
ok = owner == m.Path || m.Requires[owner]
} else {
ok = within(path, m.Path)
for r := range m.Requires {
ok = ok || within(path, r)
}
}
if !ok {
seen[path] = true
if owner == "" {
owner = "(its module)"
}
rel, _ := filepath.Rel(m.Dir, p)
out = append(out, fmt.Sprintf("%s: %s imports %s (module %s), which its go.mod does not require", filepath.Join(m.Dir, "go.mod"), rel, path, owner))
}
}
return nil
})
if err != nil {
return nil, err
}
}
sort.Strings(out)
return out, nil
}
func main() {
args := os.Args[1:]
all := len(args) > 0 && args[0] == "-all"
if all {
args = args[1:]
}
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: requiredmodules [-all] <root> [<root> ...]")
os.Exit(2)
}
report, known := args[:1], args[1:]
if all {
report, known = args, nil
}
found, err := Findings(report, known)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
for _, f := range found {
fmt.Println(f)
}
if len(found) > 0 {
os.Exit(1)
}
fmt.Println("every module requires the mesh modules it imports")
}