diff --git a/.golangci.example.yml b/.golangci.example.yml index 15d6da88c674..24ae7e5a4728 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -109,6 +109,11 @@ linters-settings: funlen: lines: 60 statements: 40 + gci: + # put imports beginning with prefix after 3rd-party packages; + # only support one prefix + # if not set, use goimports.local-prefixes + local-prefixes: github.com/org/project gocognit: # minimal code complexity to report, 30 by default (but we recommend 10-20) min-complexity: 10 diff --git a/.golangci.yml b/.golangci.yml index e8fe940feb23..5e9e1e7c2cb1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,6 +14,8 @@ linters-settings: funlen: lines: 100 statements: 50 + gci: + local-prefixes: github.com/golangci/golangci-lint goconst: min-len: 2 min-occurrences: 2 diff --git a/go.mod b/go.mod index fc9d4b33b942..d42a0e1ddfdf 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 github.com/OpenPeeDeeP/depguard v1.0.1 github.com/bombsimon/wsl/v3 v3.1.0 + github.com/daixiang0/gci v0.0.0-20200727065011-66f1df783cb2 github.com/denis-tingajkin/go-header v0.3.1 github.com/fatih/color v1.9.0 github.com/go-critic/go-critic v0.5.0 @@ -56,7 +57,7 @@ require ( github.com/ultraware/whitespace v0.0.4 github.com/uudashr/gocognit v1.0.1 github.com/valyala/quicktemplate v1.5.1 - golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1 + golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305 gopkg.in/yaml.v2 v2.3.0 honnef.co/go/tools v0.0.1-2020.1.4 mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f diff --git a/go.sum b/go.sum index 90cabc75b1a3..07036dadc4d5 100644 --- a/go.sum +++ b/go.sum @@ -46,6 +46,8 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/daixiang0/gci v0.0.0-20200727065011-66f1df783cb2 h1:3Lhhps85OdA8ezsEKu+IA1hE+DBTjt/fjd7xNCrHbVA= +github.com/daixiang0/gci v0.0.0-20200727065011-66f1df783cb2/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD/hfR4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -522,6 +524,8 @@ golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1 h1:rD1FcWVsRaMY+l8biE9jbWP5MS/CJJ/90a9TMkMgNrM= golang.org/x/tools v0.0.0-20200710042808-f1c4188a97a1/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +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/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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/pkg/config/config.go b/pkg/config/config.go index 69178580d2b7..22004bc17567 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -155,6 +155,9 @@ type Run struct { } type LintersSettings struct { + Gci struct { + LocalPrefixes string `mapstructure:"local-prefixes"` + } Govet GovetSettings Golint struct { MinConfidence float64 `mapstructure:"min-confidence"` diff --git a/pkg/golinters/gci.go b/pkg/golinters/gci.go new file mode 100644 index 000000000000..a46e650caf13 --- /dev/null +++ b/pkg/golinters/gci.go @@ -0,0 +1,77 @@ +package golinters + +import ( + "sync" + + "github.com/daixiang0/gci/pkg/gci" + "github.com/pkg/errors" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" + "github.com/golangci/golangci-lint/pkg/lint/linter" +) + +const gciName = "gci" + +func NewGci() *goanalysis.Linter { + var mu sync.Mutex + var resIssues []goanalysis.Issue + + analyzer := &analysis.Analyzer{ + Name: gciName, + Doc: goanalysis.TheOnlyanalyzerDoc, + } + return goanalysis.NewLinter( + gciName, + "Gci control golang package import order and make it always deterministic.", + []*analysis.Analyzer{analyzer}, + nil, + ).WithContextSetter(func(lintCtx *linter.Context) { + localFlag := lintCtx.Settings().Gci.LocalPrefixes + goimportsFlag := lintCtx.Settings().Goimports.LocalPrefixes + if localFlag == "" && goimportsFlag != "" { + localFlag = goimportsFlag + } + + analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { + var fileNames []string + for _, f := range pass.Files { + pos := pass.Fset.PositionFor(f.Pos(), false) + fileNames = append(fileNames, pos.Filename) + } + + var issues []goanalysis.Issue + + for _, f := range fileNames { + diff, err := gci.Run(f, &gci.FlagSet{LocalFlag: localFlag}) + if err != nil { + return nil, err + } + if diff == nil { + continue + } + + is, err := extractIssuesFromPatch(string(diff), lintCtx.Log, lintCtx, gciName) + if err != nil { + return nil, errors.Wrapf(err, "can't extract issues from gci diff output %q", string(diff)) + } + + for i := range is { + issues = append(issues, goanalysis.NewIssue(&is[i], pass)) + } + } + + if len(issues) == 0 { + return nil, nil + } + + mu.Lock() + resIssues = append(resIssues, issues...) + mu.Unlock() + + return nil, nil + } + }).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { + return resIssues + }).WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/pkg/golinters/gofmt_common.go b/pkg/golinters/gofmt_common.go index afb0d0c1bcc2..3235622e8bf5 100644 --- a/pkg/golinters/gofmt_common.go +++ b/pkg/golinters/gofmt_common.go @@ -225,6 +225,17 @@ func getErrorTextForLinter(lintCtx *linter.Context, linterName string) string { if lintCtx.Settings().Goimports.LocalPrefixes != "" { text += " with -local " + lintCtx.Settings().Goimports.LocalPrefixes } + case gciName: + text = "File is not `gci`-ed" + localPrefixes := lintCtx.Settings().Gci.LocalPrefixes + goimportsFlag := lintCtx.Settings().Goimports.LocalPrefixes + if localPrefixes == "" && goimportsFlag != "" { + localPrefixes = goimportsFlag + } + + if localPrefixes != "" { + text += " with -local " + localPrefixes + } } return text } diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index d988fb067c9f..e98a5b9aea7d 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -206,6 +206,9 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { WithPresets(linter.PresetStyle). WithLoadForGoAnalysis(). WithURL("https://github.com/denis-tingajkin/go-header"), + linter.NewConfig(golinters.NewGci()). + WithLoadForGoAnalysis(). + WithURL("https://github.com/daixiang0/gci"), linter.NewConfig(golinters.NewMaligned()). WithLoadForGoAnalysis(). WithPresets(linter.PresetPerformance). diff --git a/test/linters_test.go b/test/linters_test.go index 191744a81f49..084d431a409b 100644 --- a/test/linters_test.go +++ b/test/linters_test.go @@ -83,6 +83,22 @@ func TestGoimportsLocal(t *testing.T) { ExpectHasIssue("testdata/goimports/goimports.go:8: File is not `goimports`-ed") } +func TestGciLocal(t *testing.T) { + sourcePath := filepath.Join(testdataDir, "gci", "gci.go") + args := []string{ + "--disable-all", "--print-issued-lines=false", "--print-linter-name=false", "--out-format=line-number", + sourcePath, + } + rc := extractRunContextFromComments(t, sourcePath) + args = append(args, rc.args...) + + cfg, err := yaml.Marshal(rc.config) + assert.NoError(t, err) + + testshared.NewLintRunner(t).RunWithYamlConfig(string(cfg), args...). + ExpectHasIssue("testdata/gci/gci.go:8: File is not `gci`-ed") +} + func saveConfig(t *testing.T, cfg map[string]interface{}) (cfgPath string, finishFunc func()) { f, err := ioutil.TempFile("", "golangci_lint_test") assert.NoError(t, err) diff --git a/test/testdata/gci.go b/test/testdata/gci.go new file mode 100644 index 000000000000..4cda6e6156da --- /dev/null +++ b/test/testdata/gci.go @@ -0,0 +1,15 @@ +//args: -Egci +package testdata + +import ( + "fmt" + + "github.com/golangci/golangci-lint/pkg/config" + "github.com/pkg/errors" +) + +func GoimportsLocalTest() { + fmt.Print("x") + _ = config.Config{} + _ = errors.New("") +} diff --git a/test/testdata/gci/gci.go b/test/testdata/gci/gci.go new file mode 100644 index 000000000000..a3ab571e852b --- /dev/null +++ b/test/testdata/gci/gci.go @@ -0,0 +1,17 @@ +//args: -Egci +//config: linters-settings.gci.local-prefixes=github.com/golangci/golangci-lint +package gci + +import ( + "fmt" + + "github.com/golangci/golangci-lint/pkg/config" + + "github.com/pkg/errors" +) + +func GoimportsLocalTest() { + fmt.Print("x") + _ = config.Config{} + _ = errors.New("") +}