Repository: daixiang0/gci (go)

Issue:
panic: runtime error: index out of range [-1]
I found a testcase that causes a runtime panic in gci:
```
$ cat config/config_fuzz.go 
package config

import (
//      "os"
)
$ gci config
panic: runtime error: index out of range [-1]

goroutine 1 [running]:
github.com/daixiang0/gci/pkg/gci.(*pkg).fmt(0xc00011c0a0, 0x1, 0x1, 0x0)
        .../go/src/github.com/daixiang0/gci/pkg/gci/gci.go:130 +0xb0a
github.com/daixiang0/gci/pkg/gci.processFile(0xc000126020, 0x15, 0x51fbc0, 0xc000128008, 0xc00011c040, 0x0, 0x0)
        .../go/src/github.com/daixiang0/gci/pkg/gci/gci.go:297 +0x33a
github.com/daixiang0/gci/pkg/gci.visitFile.func1(0xc000126020, 0x15, 0x520b60, 0xc000110b60, 0x0, 0x0, 0x4aec7d, 0xc000110b60)
        .../go/src/github.com/daixiang0/gci/pkg/gci/gci.go:249 +0xb3
path/filepath.walk(0xc000126020, 0x15, 0x520b60, 0xc000110b60, 0xc000104290, 0x0, 0x0)
        .../go/go/src/path/filepath/path.go:360 +0x423
path/filepath.walk(0x7ffed58acf99, 0x6, 0x520b60, 0xc000110a90, 0xc000104290, 0x0, 0xc00010cec0)
        .../go/go/src/path/filepath/path.go:384 +0x2fe
path/filepath.Walk(0x7ffed58acf99, 0x6, 0xc000104290, 0x0, 0x0)
        .../go/go/src/path/filepath/path.go:406 +0x105
github.com/daixiang0/gci/pkg/gci.WalkDir(0x7ffed58acf99, 0x6, 0xc00011c040, 0xc0001109c0, 0x0)
        .../go/src/github.com/daixiang0/gci/pkg/gci/gci.go:256 +0x4d
main.main()
        .../go/src/github.com/daixiang0/gci/main.go:56 +0x1c5
```

AFAICS `len(ret)` is 0 in `gci/pkg/gci/gci.go`:

```
   130          if ret[len(ret)-1] == linebreak {
   131                  ret = ret[:len(ret)-1]
   132          }
```

Pull request:
Small fixes for issues found when running against our codebase
First change is to fix #7 

Second change is to fix a go file that only contained a string template used for go code generation.

Patch:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2168bd2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+dist/
+.gitcookies
+.idea/
+.vscode/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3f04977
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,18 @@
+.PHONY: clean generate test build
+
+BIN_OUTPUT := $(if $(filter $(shell go env GOOS), windows), dist/gci.exe, dist/gci)
+
+default: clean generate test build
+
+clean:
+	@echo BIN_OUTPUT: ${BIN_OUTPUT}
+	rm -rf dist/ cover.out
+
+build: clean
+	go build -v -trimpath -o ${BIN_OUTPUT} .
+
+test: clean
+	go test -v -cover ./...
+
+generate:
+	go generate ./...
diff --git a/go.mod b/go.mod
index dc7b7bb..8eae436 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,4 @@ module github.com/daixiang0/gci
 
 go 1.14
 
-require golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305
+require golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394
diff --git a/go.sum b/go.sum
index b0d64eb..bfc445e 100644
--- a/go.sum
+++ b/go.sum
@@ -1,20 +1,24 @@
-github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305 h1:yaM5S0KcY0lIoZo7Fl+oi91b/DdlU2zuWpfHrpWbCS0=
-golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394 h1:O3VD5Fds21mB1WVRTbkiz/HTXESx6Jql5ucPZi69oiM=
+golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/generate.go b/internal/generate.go
new file mode 100644
index 0000000..ca6a696
--- /dev/null
+++ b/internal/generate.go
@@ -0,0 +1,91 @@
+package main
+
+import (
+	"bytes"
+	"go/format"
+	"os"
+	"runtime"
+	"strings"
+	"text/template"
+
+	"golang.org/x/tools/go/packages"
+)
+
+//go:generate go run .
+
+const outputFile = "../pkg/gci/std.go"
+
+const stdTemplate = `
+package gci
+
+// Code generated based on {{ .Version }}. DO NOT EDIT.
+
+var standardPackages = map[string]struct{}{
+{{- range $pkg := .Packages }}
+		"{{ $pkg }}":  {},
+{{- end}}
+}
+
+func isStandardPackage(pkg string) bool {
+	_, ok := standardPackages[pkg]
+	return ok
+}
+`
+
+func main() {
+	err := generate()
+	if err != nil {
+		panic(err)
+	}
+}
+
+func generate() error {
+	all, err := packages.Load(nil, "std")
+	if err != nil {
+		return err
+	}
+
+	var pkgs []string
+
+	// go list std | grep -v vendor | grep -v internal
+	for _, pkg := range all {
+		if !strings.Contains(pkg.PkgPath, "internal") && !strings.Contains(pkg.PkgPath, "vendor") {
+			pkgs = append(pkgs, pkg.PkgPath)
+		}
+	}
+
+	file, err := os.Create(outputFile)
+	if err != nil {
+		return err
+	}
+
+	models := map[string]interface{}{
+		"Packages": pkgs,
+		"Version":  runtime.Version(),
+	}
+
+	tlt, err := template.New("std-packages").Parse(stdTemplate)
+	if err != nil {
+		return err
+	}
+
+	b := &bytes.Buffer{}
+
+	err = tlt.Execute(b, models)
+	if err != nil {
+		return err
+	}
+
+	// gofmt
+	source, err := format.Source(b.Bytes())
+	if err != nil {
+		return err
+	}
+
+	_, err = file.Write(source)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/main.go b/main.go
index 99477a9..6884ae5 100644
--- a/main.go
+++ b/main.go
@@ -3,12 +3,11 @@ package main
 import (
 	"flag"
 	"fmt"
-	"github.com/daixiang0/gci/pkg/gci"
 	"go/scanner"
 	"os"
-)
-
 
+	"github.com/daixiang0/gci/pkg/gci"
+)
 
 var (
 	doWrite = flag.Bool("w", false, "doWrite result to (source) file instead of stdout")
@@ -20,6 +19,9 @@ var (
 )
 
 func report(err error) {
+	if err == nil {
+		return
+	}
 	scanner.PrintError(os.Stderr, err)
 	exitCode = 1
 }
@@ -37,7 +39,6 @@ func usage() {
 	os.Exit(2)
 }
 
-
 func main() {
 	flag.Usage = usage
 	paths := parseFlags()
diff --git a/pkg/gci/gci.go b/pkg/gci/gci.go
index bcb99b7..47b777c 100644
--- a/pkg/gci/gci.go
+++ b/pkg/gci/gci.go
@@ -26,7 +26,6 @@ var (
 	importStartFlag = []byte(`
 import (
 `)
-
 	importEndFlag = []byte(`
 )
 `)
@@ -127,7 +126,7 @@ func (p *pkg) fmt() []byte {
 			ret = append(ret, linebreak)
 		}
 	}
-	if ret[len(ret)-1] == linebreak {
+	if len(ret) > 0 && ret[len(ret)-1] == linebreak {
 		ret = ret[:len(ret)-1]
 	}
 
@@ -158,17 +157,20 @@ func getPkgInfo(line string, comment bool) (string, string, string) {
 }
 
 func getPkgType(line, localFlag string) int {
-	if !strings.Contains(line, dot) {
-		return standard
-	} else if strings.Contains(line, localFlag) {
+	pkgName := strings.Trim(line, "\"\\`")
+
+	if localFlag != "" && strings.HasPrefix(pkgName, localFlag) {
 		return local
-	} else {
-		return remote
 	}
+
+	if isStandardPackage(pkgName) {
+		return standard
+	}
+
+	return remote
 }
 
 const (
-	dot       = "."
 	blank     = " "
 	indent    = "\t"
 	linebreak = "\n"
@@ -328,18 +330,19 @@ func processFile(filename string, out io.Writer, set *FlagSet) error {
 	return err
 }
 
-func Run(filename string, set *FlagSet) ([]byte, error) {
+// Run return source and result in []byte if succeed
+func Run(filename string, set *FlagSet) ([]byte, []byte, error) {
 	var err error
 
 	f, err := os.Open(filename)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 	defer f.Close()
 
 	src, err := ioutil.ReadAll(f)
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
 	ori := make([]byte, len(src))
@@ -347,9 +350,14 @@ func Run(filename string, set *FlagSet) ([]byte, error) {
 	start := bytes.Index(src, importStartFlag)
 	// in case no importStartFlag or importStartFlag exist in the commentFlag
 	if start < 0 {
-		return nil, nil
+		return nil, nil, nil
 	}
 	end := bytes.Index(src[start:], importEndFlag) + start
+	
+	// in case import flags are part of a codegen template, or otherwise "wrong"
+	if start+len(importStartFlag) > end {
+		return nil, nil, nil
+	}
 
 	ret := bytes.Split(src[start+len(importStartFlag):end], []byte(linebreak))
 
@@ -358,13 +366,8 @@ func Run(filename string, set *FlagSet) ([]byte, error) {
 	res := append(src[:start+len(importStartFlag)], append(p.fmt(), src[end+1:]...)...)
 
 	if bytes.Equal(ori, res) {
-		return nil, nil
-	}
-
-	data, err := diff(ori, res, filename)
-	if err != nil {
-		return nil, fmt.Errorf("failed to diff: %v", err)
+		return ori, nil, nil
 	}
 
-	return data, nil
+	return ori, res, nil
 }
diff --git a/pkg/gci/std.go b/pkg/gci/std.go
new file mode 100644
index 0000000..ac96b55
--- /dev/null
+++ b/pkg/gci/std.go
@@ -0,0 +1,161 @@
+package gci
+
+// Code generated based on go1.16beta1. DO NOT EDIT.
+
+var standardPackages = map[string]struct{}{
+	"archive/tar":          {},
+	"archive/zip":          {},
+	"bufio":                {},
+	"bytes":                {},
+	"compress/bzip2":       {},
+	"compress/flate":       {},
+	"compress/gzip":        {},
+	"compress/lzw":         {},
+	"compress/zlib":        {},
+	"container/heap":       {},
+	"container/list":       {},
+	"container/ring":       {},
+	"context":              {},
+	"crypto":               {},
+	"crypto/aes":           {},
+	"crypto/cipher":        {},
+	"crypto/des":           {},
+	"crypto/dsa":           {},
+	"crypto/ecdsa":         {},
+	"crypto/ed25519":       {},
+	"crypto/elliptic":      {},
+	"crypto/hmac":          {},
+	"crypto/md5":           {},
+	"crypto/rand":          {},
+	"crypto/rc4":           {},
+	"crypto/rsa":           {},
+	"crypto/sha1":          {},
+	"crypto/sha256":        {},
+	"crypto/sha512":        {},
+	"crypto/subtle":        {},
+	"crypto/tls":           {},
+	"crypto/x509":          {},
+	"crypto/x509/pkix":     {},
+	"database/sql":         {},
+	"database/sql/driver":  {},
+	"debug/dwarf":          {},
+	"debug/elf":            {},
+	"debug/gosym":          {},
+	"debug/macho":          {},
+	"debug/pe":             {},
+	"debug/plan9obj":       {},
+	"embed":                {},
+	"encoding":             {},
+	"encoding/ascii85":     {},
+	"encoding/asn1":        {},
+	"encoding/base32":      {},
+	"encoding/base64":      {},
+	"encoding/binary":      {},
+	"encoding/csv":         {},
+	"encoding/gob":         {},
+	"encoding/hex":         {},
+	"encoding/json":        {},
+	"encoding/pem":         {},
+	"encoding/xml":         {},
+	"errors":               {},
+	"expvar":               {},
+	"flag":                 {},
+	"fmt":                  {},
+	"go/ast":               {},
+	"go/build":             {},
+	"go/constant":          {},
+	"go/doc":               {},
+	"go/format":            {},
+	"go/importer":          {},
+	"go/parser":            {},
+	"go/printer":           {},
+	"go/scanner":           {},
+	"go/token":             {},
+	"go/types":             {},
+	"hash":                 {},
+	"hash/adler32":         {},
+	"hash/crc32":           {},
+	"hash/crc64":           {},
+	"hash/fnv":             {},
+	"hash/maphash":         {},
+	"html":                 {},
+	"html/template":        {},
+	"image":                {},
+	"image/color":          {},
+	"image/color/palette":  {},
+	"image/draw":           {},
+	"image/gif":            {},
+	"image/jpeg":           {},
+	"image/png":            {},
+	"index/suffixarray":    {},
+	"io":                   {},
+	"io/fs":                {},
+	"io/ioutil":            {},
+	"log":                  {},
+	"log/syslog":           {},
+	"math":                 {},
+	"math/big":             {},
+	"math/bits":            {},
+	"math/cmplx":           {},
+	"math/rand":            {},
+	"mime":                 {},
+	"mime/multipart":       {},
+	"mime/quotedprintable": {},
+	"net":                  {},
+	"net/http":             {},
+	"net/http/cgi":         {},
+	"net/http/cookiejar":   {},
+	"net/http/fcgi":        {},
+	"net/http/httptest":    {},
+	"net/http/httptrace":   {},
+	"net/http/httputil":    {},
+	"net/http/pprof":       {},
+	"net/mail":             {},
+	"net/rpc":              {},
+	"net/rpc/jsonrpc":      {},
+	"net/smtp":             {},
+	"net/textproto":        {},
+	"net/url":              {},
+	"os":                   {},
+	"os/exec":              {},
+	"os/signal":            {},
+	"os/user":              {},
+	"path":                 {},
+	"path/filepath":        {},
+	"plugin":               {},
+	"reflect":              {},
+	"regexp":               {},
+	"regexp/syntax":        {},
+	"runtime":              {},
+	"runtime/cgo":          {},
+	"runtime/debug":        {},
+	"runtime/metrics":      {},
+	"runtime/pprof":        {},
+	"runtime/race":         {},
+	"runtime/trace":        {},
+	"sort":                 {},
+	"strconv":              {},
+	"strings":              {},
+	"sync":                 {},
+	"sync/atomic":          {},
+	"syscall":              {},
+	"testing":              {},
+	"testing/fstest":       {},
+	"testing/iotest":       {},
+	"testing/quick":        {},
+	"text/scanner":         {},
+	"text/tabwriter":       {},
+	"text/template":        {},
+	"text/template/parse":  {},
+	"time":                 {},
+	"time/tzdata":          {},
+	"unicode":              {},
+	"unicode/utf16":        {},
+	"unicode/utf8":         {},
+	"unsafe":               {},
+}
+
+func isStandardPackage(pkg string) bool {
+	_, ok := standardPackages[pkg]
+	return ok
+}


Test patch:
diff --git a/pkg/gci/gci_test.go b/pkg/gci/gci_test.go
new file mode 100644
index 0000000..27b2ed3
--- /dev/null
+++ b/pkg/gci/gci_test.go
@@ -0,0 +1,48 @@
+package gci
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestGetPkgType(t *testing.T) {
+	testCases := []struct {
+		Line           string
+		LocalFlag      string
+		ExpectedResult int
+	}{
+		{Line: `"foo/pkg/bar"`, LocalFlag: "", ExpectedResult: remote},
+		{Line: `"foo/pkg/bar"`, LocalFlag: "foo", ExpectedResult: local},
+		{Line: `"foo/pkg/bar"`, LocalFlag: "bar", ExpectedResult: remote},
+		{Line: `"foo/pkg/bar"`, LocalFlag: "github.com/foo/bar", ExpectedResult: remote},
+
+		{Line: `"github.com/foo/bar"`, LocalFlag: "", ExpectedResult: remote},
+		{Line: `"github.com/foo/bar"`, LocalFlag: "foo", ExpectedResult: remote},
+		{Line: `"github.com/foo/bar"`, LocalFlag: "bar", ExpectedResult: remote},
+		{Line: `"github.com/foo/bar"`, LocalFlag: "github.com/foo/bar", ExpectedResult: local},
+
+		{Line: `"context"`, LocalFlag: "", ExpectedResult: standard},
+		{Line: `"context"`, LocalFlag: "context", ExpectedResult: local},
+		{Line: `"context"`, LocalFlag: "foo", ExpectedResult: standard},
+		{Line: `"context"`, LocalFlag: "bar", ExpectedResult: standard},
+		{Line: `"context"`, LocalFlag: "github.com/foo/bar", ExpectedResult: standard},
+
+		{Line: `"os/signal"`, LocalFlag: "", ExpectedResult: standard},
+		{Line: `"os/signal"`, LocalFlag: "os/signal", ExpectedResult: local},
+		{Line: `"os/signal"`, LocalFlag: "foo", ExpectedResult: standard},
+		{Line: `"os/signal"`, LocalFlag: "bar", ExpectedResult: standard},
+		{Line: `"os/signal"`, LocalFlag: "github.com/foo/bar", ExpectedResult: standard},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(fmt.Sprintf("%s:%s", tc.Line, tc.LocalFlag), func(t *testing.T) {
+			t.Parallel()
+
+			result := getPkgType(tc.Line, tc.LocalFlag)
+			if got, want := result, tc.ExpectedResult; got != want {
+				t.Errorf("bad result: %d, expected: %d", got, want)
+			}
+		})
+	}
+}