Skip to content

Commit

Permalink
🌱 parse go.mod with modfile.Parse
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandre Mahdhaoui <alexandre.mahdhaoui@gmail.com>
  • Loading branch information
alexandremahdhaoui committed Apr 27, 2024
1 parent d09c61e commit 6449beb
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 66 deletions.
File renamed without changes.
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ $(GOLANGCI_LINT): # Build golangci-lint from tools folder.

GO_MOD_CHECK_DIR := $(abspath ./hack/tools/cmd/gomodcheck)
GO_MOD_CHECK := $(abspath $(TOOLS_BIN_DIR)/gomodcheck)
GO_MOD_CHECK_IGNORE := $(abspath ./hack/.gomodcheck.yaml)
GO_MOD_CHECK_IGNORE := $(abspath .gomodcheck.yaml)
.PHONY: $(GO_MOD_CHECK)
$(GO_MOD_CHECK): # Build gomodcheck
go build -C $(GO_MOD_CHECK_DIR) -o $(GO_MOD_CHECK)
Expand Down Expand Up @@ -149,5 +149,3 @@ APIDIFF_OLD_COMMIT ?= $(shell git rev-parse origin/main)
.PHONY: apidiff
verify-apidiff: $(GO_APIDIFF) ## Check for API differences
$(GO_APIDIFF) $(APIDIFF_OLD_COMMIT) --print-compatible


2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ require (
sigs.k8s.io/yaml v1.3.0
)

require golang.org/x/mod v0.15.0

require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6R
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
Expand Down
Empty file removed hack/tools/.keep
Empty file.
123 changes: 60 additions & 63 deletions hack/tools/cmd/gomodcheck/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"

"go.uber.org/zap"
"golang.org/x/mod/modfile"
"sigs.k8s.io/yaml"
)

Expand All @@ -17,8 +17,8 @@ const (
)

type config struct {
UpstreamRefs []string `yaml:"upstreamRefs"`
ExcludedModules []string `yaml:"excludedModules"`
UpstreamRefs []string `json:"upstreamRefs"`
ExcludedModules []string `json:"excludedModules"`
}

type upstream struct {
Expand All @@ -45,12 +45,12 @@ func main() {
// --- 1. parse config
b, err := os.ReadFile(os.Args[1])
if err != nil {
logger.Fatal(err.Error())
fatal(err)
}

cfg := new(config)
if err := yaml.Unmarshal(b, cfg); err != nil {
logger.Fatal(err.Error())
fatal(err)
}

excludedMods := make(map[string]any)
Expand All @@ -59,15 +59,15 @@ func main() {
}

// --- 2. project mods
deps, err := parseModFile()
projectModules, err := modulesFromGoModFile()
if err != nil {
logger.Fatal(err.Error())
fatal(err)
}

// --- 3. upstream mods (holding upstream refs)
upstreamModGraph, err := getUpstreamModGraph(cfg.UpstreamRefs)
// --- 3. upstream mods
upstreamModules, err := modulesFromUpstreamModGraph(cfg.UpstreamRefs)
if err != nil {
logger.Fatal(err.Error())
fatal(err)
}

oosMods := make([]oosMod, 0)
Expand All @@ -78,13 +78,13 @@ func main() {
// then for each upstream module,
// if project module version doesn't match upstream version,
// then we add the version and the ref to the list of out of sync modules.
for mod, version := range deps {
for mod, version := range projectModules {
if _, ok := excludedMods[mod]; ok {
logger.Infof("skipped excluded module: %s", mod)
continue
}

if versionToRef, ok := upstreamModGraph[mod]; ok {
if versionToRef, ok := upstreamModules[mod]; ok {
upstreams := make([]upstream, 0)

for upstreamVersion, upstreamRef := range versionToRef {
Expand All @@ -107,97 +107,94 @@ func main() {
}

if len(oosMods) == 0 {
fmt.Println("Success! 🎉")
fmt.Println("🎉 Success!")
os.Exit(0)
}

b, err = json.MarshalIndent(map[string]any{"outOfSyncModules": oosMods}, "", " ")
if err != nil {
panic(err)
fatal(err)
}

fmt.Println(string(b))
os.Exit(1)
}

var (
cleanMods = regexp.MustCompile(`\t| *//.*`)
modDelimStart = regexp.MustCompile(`^require.*`)
modDelimEnd = ")"
)

func parseModFile() (map[string]string, error) {
func modulesFromGoModFile() (map[string]string, error) {
b, err := os.ReadFile(modFile)
if err != nil {
return nil, err
}

in := string(cleanMods.ReplaceAll(b, []byte("")))
out := make(map[string]string)

start := false
for _, s := range strings.Split(in, "\n") {
switch {
case modDelimStart.MatchString(s) && !start:
start = true
case s == modDelimEnd:
return out, nil
case start:
kv := strings.SplitN(s, " ", 2)
if len(kv) < 2 {
return nil, fmt.Errorf("unexpected format for module: %q", s)
}
f, err := modfile.Parse(modFile, b, nil)
if err != nil {
return nil, err
}

out[kv[0]] = kv[1]
}
out := make(map[string]string)
for _, mod := range f.Require {
out[mod.Mod.Path] = mod.Mod.Version
}

return out, nil
}

func getUpstreamModGraph(upstreamRefs []string) (map[string]map[string]string, error) {
func modulesFromUpstreamModGraph(upstreamRefList []string) (map[string]map[string]string, error) {
b, err := exec.Command("go", "mod", "graph").Output()
if err != nil {
return nil, err
}

graph := string(b)
o1Refs := make(map[string]bool)
for _, upstreamRef := range upstreamRefs {
o1Refs[upstreamRef] = false

// upstreamRefs is a set of user specified upstream modules.
// The set has 2 functions:
// 1. Check if `go mod graph` modules are one of the user specified upstream modules.
// 2. Mark if a user specified upstream module was found in the module graph.
// If a user specified upstream module is not found, gomodcheck will exit with an error.
upstreamRefs := make(map[string]bool)
for _, ref := range upstreamRefList {
upstreamRefs[ref] = false
}

modToVersionToUpstreamRef := make(map[string]map[string]string)

for _, line := range strings.Split(graph, "\n") {
upstreamRef := strings.SplitN(line, "@", 2)[0]
if _, ok := o1Refs[upstreamRef]; ok {
o1Refs[upstreamRef] = true
kv := strings.SplitN(strings.SplitN(line, " ", 2)[1], "@", 2)
name := kv[0]
version := kv[1]

if m, ok := modToVersionToUpstreamRef[kv[0]]; ok {
m[version] = upstreamRef
} else {
versionToRef := map[string]string{version: upstreamRef}
modToVersionToUpstreamRef[name] = versionToRef
}
ref := strings.SplitN(line, "@", 2)[0]

if _, ok := upstreamRefs[ref]; !ok {
continue
}

upstreamRefs[ref] = true // mark the ref as found

kv := strings.SplitN(strings.SplitN(line, " ", 2)[1], "@", 2)
name := kv[0]
version := kv[1]

if _, ok := modToVersionToUpstreamRef[name]; !ok {
modToVersionToUpstreamRef[name] = make(map[string]string)
}

modToVersionToUpstreamRef[name][version] = ref
}

notFound := ""
for ref, found := range o1Refs {
notFoundErr := ""
for ref, found := range upstreamRefs {
if !found {
notFound = fmt.Sprintf("%s%s, ", notFound, ref)
notFoundErr = fmt.Sprintf("%s%s, ", notFoundErr, ref)
}
}

if notFound != "" {
return nil, fmt.Errorf("cannot verify modules;"+
"the following specified upstream module cannot be found in go.mod: [ %s ]",
strings.TrimSuffix(notFound, ", "))
if notFoundErr != "" {
return nil, fmt.Errorf("cannot verify modules: "+
"the following specified upstream module(s) cannot be found in go.mod: [ %s ]",
strings.TrimSuffix(notFoundErr, ", "))
}

return modToVersionToUpstreamRef, nil
}

func fatal(err error) {
fmt.Printf("❌ %s\n", err.Error())
os.Exit(1)
}

0 comments on commit 6449beb

Please sign in to comment.