Skip to content

Commit

Permalink
feat(check-plugin): apply fixes flag
Browse files Browse the repository at this point in the history
Add --fix / -x which automatically applies go.mod fixes instead of
just printing them out.
  • Loading branch information
stevenh committed Aug 23, 2024
1 parent a7448e2 commit b521d20
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 66 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.9.0
golang.org/x/mod v0.12.0
golang.org/x/mod v0.19.0
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
128 changes: 101 additions & 27 deletions plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ import (
"golang.org/x/mod/modfile"
)

// indirectRequires returns the indirect dependencies of the go.sum file.
func indirectRequires(goSum string) (map[string]struct{}, error) {
dir := filepath.Dir(goSum)
filename := filepath.Join(dir, "go.mod")
// goMod returns the go.mod file path from the go.sum file path.
func goMod(goSum string) string {
return filepath.Join(filepath.Dir(goSum), "go.mod")
}

// indirectRequires returns the details and indirect dependencies of the go.sum file.
func indirectRequires(goSum string) (*modfile.File, map[string]struct{}, error) {
filename := goMod(goSum)
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("reading go.mod: %w", err)
return nil, nil, fmt.Errorf("read go.mod: %w", err)
}

f, err := modfile.Parse(filename, data, nil)
if err != nil {
return nil, fmt.Errorf("parsing go.mod: %w", err)
return nil, nil, fmt.Errorf("parse go.mod: %w", err)
}

indirects := map[string]struct{}{}
Expand All @@ -31,7 +35,23 @@ func indirectRequires(goSum string) (map[string]struct{}, error) {
}
}

return indirects, nil
return f, indirects, nil
}

// writeModFile writes the modfile.File to the go.mod file determined from goSum.
func writeModFile(goSum string, f *modfile.File) error {
f.Cleanup()
data, err := f.Format()
if err != nil {
return fmt.Errorf("format go.mod: %w", err)
}

filename := goMod(goSum)
if err = os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("write go.sum: %w", err)
}

return nil
}

// getBuildInfo returns the dependencies of the binary calling it.
Expand Down Expand Up @@ -66,32 +86,86 @@ func pluginFuncErr(cmd *cobra.Command, _ []string) error {
return nil
}

if gogetEnabled {
indirects, err := indirectRequires(goSum)
if err != nil {
var indirects map[string]struct{}
var modFile *modfile.File
if gogetEnabled || fixEnabled {
if modFile, indirects, err = indirectRequires(goSum); err != nil {
return err
}
for _, diff := range diffs {
if diff.Name != "go" && diff.Name != "libc" {
if _, ok := indirects[diff.Name]; ok {
cmd.Printf("go mod edit --replace %s=%s@%s\n", diff.Name, diff.Name, diff.Expected)
} else {
cmd.Printf("go get %s@%s\n", diff.Name, diff.Expected)
}
continue
}

fixed, err := outputOrFix(cmd, diffs, modFile, indirects)
if err != nil {
return err
}

// Report any remaining incompatibilities.
if len(diffs) != fixed {
if fixed > 0 {
return fmt.Errorf("%d incompatibilities fixed, %d left", fixed, len(diffs)-fixed)
}

return fmt.Errorf("%d incompatibilities found", len(diffs))
}

return nil
}

// outputOrFix prints the incompatibilities or applies the fixes if fixEnabled is true.
// It returns the number of incompatibilities fixed.
func outputOrFix(cmd *cobra.Command, diffs []plugin.Diff, modFile *modfile.File, indirects map[string]struct{}) (int, error) {
var fixed int
for _, diff := range diffs {
if diff.Name != "go" && diff.Name != "libc" && (gogetEnabled || fixEnabled) {
if ok, err := outputOrFixDiff(cmd, diff, modFile, indirects); err != nil {
return 0, err
} else if ok {
fixed++
}
continue
}

cmd.Println(diff.Name)
cmd.Println("\thave:", diff.Have)
cmd.Println("\twant:", diff.Expected)
}

if fixed > 0 {
if err := writeModFile(goSum, modFile); err != nil {
return 0, err
}

cmd.Printf("%d incompatibilities fixed\n", fixed)
}

return fixed, nil
}

cmd.Println(diff.Name)
cmd.Println("\thave:", diff.Have)
cmd.Println("\twant:", diff.Expected)
// outputOrFixDiff prints the commands to fix the incompatibility or applies
// the fix if fixEnabled is true for the given diff.
// It returns true if the incompatibility was fixed.
func outputOrFixDiff(cmd *cobra.Command, diff plugin.Diff, modFile *modfile.File, indirects map[string]struct{}) (bool, error) {
if _, ok := indirects[diff.Name]; ok {
if fixEnabled {
if err := modFile.AddReplace(diff.Name, "", diff.Name, diff.Expected); err != nil {
return false, fmt.Errorf("add replace: %w", err)
}
return true, nil
}
} else {
for _, diff := range diffs {
cmd.Println(diff.Name)
cmd.Println("\thave:", diff.Have)
cmd.Println("\twant:", diff.Expected)

cmd.Printf("go mod edit --replace %s=%s@%s\n", diff.Name, diff.Name, diff.Expected)

return false, nil
}

if fixEnabled {
if err := modFile.AddRequire(diff.Name, diff.Expected); err != nil {
return false, fmt.Errorf("add require: %w", err)
}

return true, nil
}
cmd.Printf("go get %s@%s\n", diff.Name, diff.Expected)

return fmt.Errorf("%d incompatibilities found", len(diffs))
return false, nil
}
150 changes: 115 additions & 35 deletions plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,60 @@ package cmd

import (
"bytes"
"errors"
"os"
"path/filepath"
"strings"
"testing"

"github.com/krakendio/krakend-cobra/v2/plugin"
"github.com/luraproject/lura/v2/core"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
)

const testDir = "testdata"

// copyDir is a helper function to copy directory entries from src to dst.
func copyDir(t *testing.T, srcSubDir, dstDir string) {
t.Helper()

srcDir := filepath.Join(testDir, srcSubDir)
entries, err := os.ReadDir(srcDir)
if errors.Is(err, os.ErrNotExist) {
// Nothing to do.
return
}
require.NoError(t, err)

for _, entry := range entries {
file := entry.Name()
data, err := os.ReadFile(filepath.Join(srcDir, file))
require.NoError(t, err)

err = os.WriteFile(filepath.Join(dstDir, file), data, 0644)
require.NoError(t, err)
}
}

// loadFile is a helper function to load a file from the testdata directory.
func loadFile(t *testing.T, name string) string {
t.Helper()
data, err := os.ReadFile(filepath.Join(testDir, name))
require.NoError(t, err)

return string(data)
}

func Test_pluginFuncErr(t *testing.T) {
var buf bytes.Buffer
cmd := &cobra.Command{}
cmd.SetOutput(&buf)

localDescriber = func() plugin.Descriptor {
return plugin.Descriptor{
Go: goVersion,
Libc: libcVersion,
Go: core.GoVersion,
Libc: core.GlibcVersion,
Deps: map[string]string{
"golang.org/x/mod": "v0.6.0-dev.0.20220419223038-86c51ed26bb4",
"github.com/Azure/azure-sdk-for-go": "v59.3.0+incompatible",
Expand All @@ -28,63 +66,105 @@ func Test_pluginFuncErr(t *testing.T) {

defer func() { localDescriber = plugin.Local }()

goModData := loadFile(t, "go.mod")

tests := map[string]struct {
goSum string
expected string
fix bool
err string
dir string
expected string
expectedGoMod string
goVersion string
format bool
fix bool
err string
}{

"missing": {
goSum: "./testdata/missing-go.sum",
err: "open ./testdata/missing-go.sum: no such file or directory",
dir: "missing",
goVersion: goVersion,
expectedGoMod: goModData,
err: "open DIR/go.sum: no such file or directory",
},
"matching": {
goSum: "./testdata/match-go.sum",
expected: "No incompatibilities found!\n",

"match": {
dir: "match",
goVersion: goVersion,
expectedGoMod: goModData,
expected: "No incompatibilities found!\n",
},
"changes": {
goSum: "./testdata/changes-go.sum",
expected: `cloud.google.com/go
have: v0.100.3
want: v0.100.2
github.com/Azure/azure-sdk-for-go
have: v59.3.1+incompatible
want: v59.3.0+incompatible
golang.org/x/mod
have: v0.6.10-dev.0.20220419223038-86c51ed26bb4
want: v0.6.0-dev.0.20220419223038-86c51ed26bb4
`,
err: "3 incompatibilities found",
dir: "changes",
goVersion: goVersion,
expectedGoMod: goModData,
expected: loadFile(t, "changes/expected.txt"),
err: "3 incompatibilities found",
},
"fix": {
goSum: "./testdata/changes-go.sum",
fix: true,
expected: `go mod edit --replace cloud.google.com/go=cloud.google.com/go@v0.100.2
go mod edit --replace github.com/Azure/azure-sdk-for-go=github.com/Azure/azure-sdk-for-go@v59.3.0+incompatible
go get golang.org/x/mod@v0.6.0-dev.0.20220419223038-86c51ed26bb4
"format": {
dir: "changes",
goVersion: goVersion,
expectedGoMod: goModData,
format: true,
expected: loadFile(t, "format/expected.txt"),
err: "3 incompatibilities found",
},
"fixed-all": {
dir: "changes",
goVersion: goVersion,
expectedGoMod: loadFile(t, "fixed-all/go.mod"),
fix: true,
expected: "3 incompatibilities fixed\n",
},
"fixed-some": {
dir: "changes",
goVersion: "1.1.0",
expectedGoMod: loadFile(t, "fixed-some/go.mod"),
fix: true,
expected: `go
have: 1.1.0
want: undefined
3 incompatibilities fixed
`,
err: "3 incompatibilities found",
err: "3 incompatibilities fixed, 1 left",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
buf.Reset()

// Make copies in a temporary directory so
// the original files are not modified.
tempDir := t.TempDir()
orig := goSum
goSum = tc.goSum
goSum = filepath.Join(tempDir, "go.sum")
copyDir(t, tc.dir, tempDir)
defer func() { goSum = orig }()

fix := gogetEnabled
gogetEnabled = tc.fix
defer func() { gogetEnabled = fix }()
// Override the global variables for the test.
format := gogetEnabled
fix := fixEnabled
gogetEnabled = tc.format
fixEnabled = tc.fix
oldGoVersion := goVersion
goVersion = tc.goVersion
defer func() {
gogetEnabled = format
fixEnabled = fix
goVersion = oldGoVersion
}()

err := pluginFuncErr(cmd, nil)
if tc.err != "" {
require.EqualError(t, err, tc.err)
require.EqualError(t, err, strings.ReplaceAll(tc.err, "DIR", tempDir))
} else {
require.NoError(t, err)
}
require.Equal(t, tc.expected, buf.String())

data, err := os.ReadFile(filepath.Join(tempDir, "go.mod"))
if errors.Is(err, os.ErrNotExist) {
return
}
require.NoError(t, err)
require.Equal(t, string(tc.expectedGoMod), string(data))
})
}
}
Loading

0 comments on commit b521d20

Please sign in to comment.