// 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 . [ ...] // // 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] [ ...]") 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") }