github.com/zuoyebang/bitalostable@v1.0.1-0.20240229032404-e3b99a834294/internal/lint/lint_test.go (about)

     1  // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
     2  // of this source code is governed by a BSD-style license that can be found in
     3  // the LICENSE file.
     4  
     5  package lint
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"go/build"
    11  	"os/exec"
    12  	"regexp"
    13  	"runtime"
    14  	"strings"
    15  	"testing"
    16  
    17  	"github.com/cockroachdb/errors"
    18  	"github.com/ghemawat/stream"
    19  	"github.com/hashicorp/go-version"
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  const (
    24  	cmdGo       = "go"
    25  	golint      = "golang.org/x/lint/golint@6edffad5e6160f5949cdefc81710b2706fbcd4f6"
    26  	staticcheck = "honnef.co/go/tools/cmd/staticcheck@2022.1.2"
    27  	crlfmt      = "github.com/cockroachdb/crlfmt@b3eff0b"
    28  )
    29  
    30  func dirCmd(t *testing.T, dir string, name string, args ...string) stream.Filter {
    31  	cmd := exec.Command(name, args...)
    32  	cmd.Dir = dir
    33  	out, err := cmd.CombinedOutput()
    34  	switch err.(type) {
    35  	case nil:
    36  	case *exec.ExitError:
    37  		// Non-zero exit is expected.
    38  	default:
    39  		require.NoError(t, err)
    40  	}
    41  	return stream.ReadLines(bytes.NewReader(out))
    42  }
    43  
    44  func ignoreGoMod() stream.Filter {
    45  	return stream.GrepNot(`^go: (finding|extracting|downloading)`)
    46  }
    47  
    48  func TestLint(t *testing.T) {
    49  	if runtime.GOOS == "windows" {
    50  		t.Skip("lint checks skipped on Windows")
    51  	}
    52  
    53  	const root = "github.com/zuoyebang/bitalostable"
    54  
    55  	pkg, err := build.Import(root, "../..", 0)
    56  	require.NoError(t, err)
    57  
    58  	var pkgs []string
    59  	if err := stream.ForEach(
    60  		stream.Sequence(
    61  			dirCmd(t, pkg.Dir, "go", "list", "./..."),
    62  			ignoreGoMod(),
    63  		), func(s string) {
    64  			pkgs = append(pkgs, s)
    65  		}); err != nil {
    66  		require.NoError(t, err)
    67  	}
    68  
    69  	t.Run("TestGolint", func(t *testing.T) {
    70  		// Go versions less than 1.17 do not support the `go run path@version`
    71  		// syntax.
    72  		// TODO(travers): This can be removed when support for go1.16 is dropped.
    73  		if goVersionMatches("< 1.17") {
    74  			t.Skip("go run path@version unsupported in versions < go1.17")
    75  		}
    76  		t.Parallel()
    77  
    78  		args := []string{"run", golint}
    79  		args = append(args, pkgs...)
    80  
    81  		// This is overkill right now, but provides a structure for filtering out
    82  		// lint errors we don't care about.
    83  		if err := stream.ForEach(
    84  			stream.Sequence(
    85  				dirCmd(t, pkg.Dir, cmdGo, args...),
    86  				stream.GrepNot("go: downloading"),
    87  			), func(s string) {
    88  				t.Errorf("\n%s", s)
    89  			}); err != nil {
    90  			t.Error(err)
    91  		}
    92  	})
    93  
    94  	t.Run("TestStaticcheck", func(t *testing.T) {
    95  		// Go versions less than 1.17 do not support the `go run path@version`
    96  		// syntax.
    97  		// TODO(travers): This can be removed when support for go1.16 is dropped.
    98  		if goVersionMatches("< 1.17") {
    99  			t.Skip("go run path@version unsupported in versions < go1.17")
   100  		}
   101  		t.Parallel()
   102  
   103  		args := []string{"run", staticcheck}
   104  		args = append(args, pkgs...)
   105  
   106  		if err := stream.ForEach(
   107  			stream.Sequence(
   108  				dirCmd(t, pkg.Dir, cmdGo, args...),
   109  				stream.GrepNot("go: downloading"),
   110  			), func(s string) {
   111  				t.Errorf("\n%s", s)
   112  			}); err != nil {
   113  			t.Error(err)
   114  		}
   115  	})
   116  
   117  	t.Run("TestGoVet", func(t *testing.T) {
   118  		t.Parallel()
   119  
   120  		if err := stream.ForEach(
   121  			stream.Sequence(
   122  				dirCmd(t, pkg.Dir, "go", "vet", "-all", "./..."),
   123  				stream.GrepNot(`^#`), // ignore comment lines
   124  				ignoreGoMod(),
   125  			), func(s string) {
   126  				t.Errorf("\n%s", s)
   127  			}); err != nil {
   128  			t.Error(err)
   129  		}
   130  	})
   131  
   132  	t.Run("TestFmtErrorf", func(t *testing.T) {
   133  		t.Parallel()
   134  
   135  		if err := stream.ForEach(
   136  			stream.Sequence(
   137  				dirCmd(t, pkg.Dir, "git", "grep", "fmt\\.Errorf("),
   138  				stream.GrepNot(`^vendor/`), // ignore vendor
   139  			), func(s string) {
   140  				t.Errorf("\n%s <- please use \"errors.Errorf\" instead", s)
   141  			}); err != nil {
   142  			t.Error(err)
   143  		}
   144  	})
   145  
   146  	t.Run("TestOSIsErr", func(t *testing.T) {
   147  		t.Parallel()
   148  
   149  		if err := stream.ForEach(
   150  			stream.Sequence(
   151  				dirCmd(t, pkg.Dir, "git", "grep", "os\\.Is"),
   152  				stream.GrepNot(`^vendor/`), // ignore vendor
   153  			), func(s string) {
   154  				t.Errorf("\n%s <- please use the \"oserror\" equivalent instead", s)
   155  			}); err != nil {
   156  			t.Error(err)
   157  		}
   158  	})
   159  
   160  	t.Run("TestSetFinalizer", func(t *testing.T) {
   161  		t.Parallel()
   162  
   163  		if err := stream.ForEach(
   164  			stream.Sequence(
   165  				dirCmd(t, pkg.Dir, "git", "grep", "-B1", "runtime\\.SetFinalizer("),
   166  				lintIgnore("lint:ignore SetFinalizer"),
   167  				stream.GrepNot(`^vendor/`), // ignore vendor
   168  				stream.GrepNot(`^internal/invariants/finalizer_on.go`),
   169  			), func(s string) {
   170  				t.Errorf("\n%s <- please use the \"invariants.SetFinalizer\" equivalent instead", s)
   171  			}); err != nil {
   172  			t.Error(err)
   173  		}
   174  	})
   175  
   176  	t.Run("TestForbiddenImports", func(t *testing.T) {
   177  		t.Parallel()
   178  
   179  		// Forbidden-import-pkg -> permitted-replacement-pkg
   180  		forbiddenImports := map[string]string{
   181  			"errors":     "github.com/cockroachdb/errors",
   182  			"pkg/errors": "github.com/cockroachdb/errors",
   183  		}
   184  
   185  		// grepBuf creates a grep string that matches any forbidden import pkgs.
   186  		var grepBuf bytes.Buffer
   187  		grepBuf.WriteByte('(')
   188  		for forbiddenPkg := range forbiddenImports {
   189  			grepBuf.WriteByte('|')
   190  			grepBuf.WriteString(regexp.QuoteMeta(forbiddenPkg))
   191  		}
   192  		grepBuf.WriteString(")$")
   193  
   194  		filter := stream.FilterFunc(func(arg stream.Arg) error {
   195  			for _, path := range pkgs {
   196  				buildContext := build.Default
   197  				buildContext.UseAllFiles = true
   198  				importPkg, err := buildContext.Import(path, pkg.Dir, 0)
   199  				if _, ok := err.(*build.MultiplePackageError); ok {
   200  					buildContext.UseAllFiles = false
   201  					importPkg, err = buildContext.Import(path, pkg.Dir, 0)
   202  				}
   203  
   204  				switch err.(type) {
   205  				case nil:
   206  					for _, s := range importPkg.Imports {
   207  						arg.Out <- importPkg.ImportPath + ": " + s
   208  					}
   209  					for _, s := range importPkg.TestImports {
   210  						arg.Out <- importPkg.ImportPath + ": " + s
   211  					}
   212  					for _, s := range importPkg.XTestImports {
   213  						arg.Out <- importPkg.ImportPath + ": " + s
   214  					}
   215  				case *build.NoGoError:
   216  				default:
   217  					return errors.Wrapf(err, "error loading package %s", path)
   218  				}
   219  			}
   220  			return nil
   221  		})
   222  		if err := stream.ForEach(stream.Sequence(
   223  			filter,
   224  			stream.Sort(),
   225  			stream.Uniq(),
   226  			stream.Grep(grepBuf.String()),
   227  		), func(s string) {
   228  			pkgStr := strings.Split(s, ": ")
   229  			importedPkg := pkgStr[1]
   230  
   231  			// Test that a disallowed package is not imported.
   232  			if replPkg, ok := forbiddenImports[importedPkg]; ok {
   233  				t.Errorf("\n%s <- please use %q instead of %q", s, replPkg, importedPkg)
   234  			}
   235  		}); err != nil {
   236  			t.Error(err)
   237  		}
   238  	})
   239  
   240  	t.Run("TestCrlfmt", func(t *testing.T) {
   241  		// Go versions less than 1.17 do not support the `go run path@version`
   242  		// syntax.
   243  		// TODO(travers): This can be removed when support for go1.16 is dropped.
   244  		if goVersionMatches("< 1.17") {
   245  			t.Skip("go run path@version unsupported in versions < go1.17")
   246  		}
   247  		t.Parallel()
   248  
   249  		args := []string{"run", crlfmt, "-fast", "-tab", "2", "-ignore", "^vendor/", "."}
   250  		var buf bytes.Buffer
   251  		if err := stream.ForEach(
   252  			stream.Sequence(
   253  				dirCmd(t, pkg.Dir, cmdGo, args...),
   254  				stream.GrepNot("go: downloading"),
   255  			),
   256  			func(s string) {
   257  				fmt.Fprintln(&buf, s)
   258  			}); err != nil {
   259  			t.Error(err)
   260  		}
   261  		errs := buf.String()
   262  		if len(errs) > 0 {
   263  			t.Errorf("\n%s", errs)
   264  		}
   265  
   266  		if t.Failed() {
   267  			reWriteCmd := []string{crlfmt, "-w"}
   268  			reWriteCmd = append(reWriteCmd, args...)
   269  			t.Logf("run the following to fix your formatting:\n"+
   270  				"\n%s\n\n"+
   271  				"Don't forget to add amend the result to the correct commits.",
   272  				strings.Join(reWriteCmd, " "),
   273  			)
   274  		}
   275  	})
   276  }
   277  
   278  // lintIgnore is a stream.FilterFunc that filters out lines that are preceded by
   279  // the given ignore directive. The function assumes the input stream receives a
   280  // sequence of strings that are to be considered as pairs. If the first string
   281  // in the sequence matches the ignore directive, the following string is
   282  // dropped, else it is emitted.
   283  //
   284  // For example, given the sequence "foo", "bar", "baz", "bam", and an ignore
   285  // directive "foo", the sequence "baz", "bam" would be emitted. If the directive
   286  // was "baz", the sequence "foo", "bar" would be emitted.
   287  func lintIgnore(ignore string) stream.FilterFunc {
   288  	return func(arg stream.Arg) error {
   289  		var prev string
   290  		var i int
   291  		for s := range arg.In {
   292  			if i%2 == 0 {
   293  				// Fist string in the pair is used as the filter. Store it.
   294  				prev = s
   295  			} else {
   296  				// Second string is emitted only if it _does not_ match the directive.
   297  				if !strings.Contains(prev, ignore) {
   298  					arg.Out <- s
   299  				}
   300  			}
   301  			i++
   302  		}
   303  		return nil
   304  	}
   305  }
   306  
   307  // goVersionMatches returns true if the Go runtime versions matches the given
   308  // semver constraint. If the runtime version does not contain a semver string
   309  // (i.e. it is a SHA), this function returns false.
   310  func goVersionMatches(constraint string) bool {
   311  	runtimeVersion := strings.TrimPrefix(runtime.Version(), "go")
   312  	goV, err := version.NewSemver(runtimeVersion)
   313  	if err != nil {
   314  		return false // Fail open.
   315  	}
   316  	c := version.MustConstraints(version.NewConstraint(constraint))
   317  	return c.Check(goV)
   318  }