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 instead of just printing them
out.
  • Loading branch information
stevenh committed Jul 11, 2024
1 parent 1602587 commit 515c2b1
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 64 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 v0.0.5
github.com/stretchr/testify v1.9.0
golang.org/x/mod v0.8.0
golang.org/x/mod v0.19.0
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
golang.org/x/mod v0.8.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-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
114 changes: 87 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("read 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("parse 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 @@ -59,32 +79,72 @@ func pluginFunc(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
}

var fixed int
for _, diff := range diffs {
if diff.Name != "go" && diff.Name != "libc" && (gogetEnabled || fixEnabled) {
if ok, err := outputOrFix(cmd, diff, modFile, indirects); err != nil {
return 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 err
}
cmd.Printf("%d incompatibilities fixed\n", fixed)
}

cmd.Println(diff.Name)
cmd.Println("\thave:", diff.Have)
cmd.Println("\twant:", diff.Expected)
if len(diffs) != fixed {
if fixed > 0 {
return fmt.Errorf("%d incompatibilities fixed, %d remains", fixed, len(diffs)-fixed)
}
} else {
for _, diff := range diffs {
cmd.Println(diff.Name)
cmd.Println("\thave:", diff.Have)
cmd.Println("\twant:", diff.Expected)

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

return nil
}

// outputOrFix prints the commands to fix the incompatibility or applies the fix if fixEnabled is true.
// It returns true if the incompatibility was fixed.
func outputOrFix(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
}

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_pluginFunc(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_pluginFunc(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/[email protected]
go mod edit --replace github.com/Azure/azure-sdk-for-go=github.com/Azure/[email protected]+incompatible
go get golang.org/x/[email protected]
"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 remains",
},
}
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 := pluginFunc(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))
})
}
}
4 changes: 3 additions & 1 deletion root.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
libcVersion = core.GlibcVersion
checkDumpPrefix = "\t"
gogetEnabled = false
fixEnabled = false

DefaultRoot Root
RootCommand Command
Expand Down Expand Up @@ -110,13 +111,14 @@ func init() {
goVersionFlag := StringFlagBuilder(&goVersion, "go", "g", goVersion, "The version of the go compiler used for your plugin")
libcVersionFlag := StringFlagBuilder(&libcVersion, "libc", "l", "", "Version of the libc library used")
gogetFlag := BoolFlagBuilder(&gogetEnabled, "format", "f", false, "Shows fix commands to update your dependencies")
fixFlag := BoolFlagBuilder(&fixEnabled, "fix", "x", false, "Applies fixes to update your dependencies")
PluginCommand = NewCommand(pluginCmd, goSumFlag, goVersionFlag, libcVersionFlag, gogetFlag)

rulesToExcludeFlag := StringFlagBuilder(&rulesToExclude, "ignore", "i", rulesToExclude, "List of rules to ignore (comma-separated, no spaces)")
severitiesToIncludeFlag := StringFlagBuilder(&severitiesToInclude, "severity", "s", severitiesToInclude, "List of severities to include (comma-separated, no spaces)")
pathToRulesToExcludeFlag := StringFlagBuilder(&rulesToExcludePath, "ignore-file", "I", rulesToExcludePath, "Path to a text-plain file containing the list of rules to exclude")
formatFlag := StringFlagBuilder(&formatTmpl, "format", "f", formatTmpl, "Inline go template to render the results")
AuditCommand = NewCommand(auditCmd, cfgFlag, rulesToExcludeFlag, severitiesToIncludeFlag, pathToRulesToExcludeFlag, formatFlag)
AuditCommand = NewCommand(auditCmd, cfgFlag, rulesToExcludeFlag, severitiesToIncludeFlag, pathToRulesToExcludeFlag, formatFlag, fixFlag)

VersionCommand = NewCommand(versionCmd)

Expand Down
Loading

0 comments on commit 515c2b1

Please sign in to comment.