diff --git a/.golangci.example.yml b/.golangci.example.yml index 1b06635f..36f8ed40 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -96,6 +96,9 @@ linters-settings: gocognit: # minimal code complexity to report, 30 by default (but we recommend 10-20) min-complexity: 10 + nestif: + # minimal complexity of if statements to report, 5 by default + min-complexity: 4 goconst: # minimal length of string constant, 3 by default min-len: 3 diff --git a/README.md b/README.md index 8263cf9f..8fcd03a6 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ lll: Reports long lines [fast: true, auto-fix: false] maligned: Tool to detect Go structs that would take less memory if their fields were sorted [fast: true, auto-fix: false] misspell: Finds commonly misspelled English words in comments [fast: true, auto-fix: true] nakedret: Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] +nestif: Reports deeply nested if statements [fast: true, auto-fix: false] prealloc: Finds slice declarations that could potentially be preallocated [fast: true, auto-fix: false] rowserrcheck: checks whether Err of rows is checked successfully [fast: true, auto-fix: false] scopelint: Scopelint checks for unpinned variables in go programs [fast: true, auto-fix: false] @@ -492,6 +493,7 @@ golangci-lint help linters - [gomodguard](https://github.com/ryancurrah/gomodguard) - Allow and block list linter for direct Go module dependencies. - [godot](https://github.com/tetafro/godot) - Check if comments end in a period - [testpackage](https://github.com/maratori/testpackage) - linter that makes you use a separate _test package +- [nestif](https://github.com/nakabonne/nestif) - Reports deeply nested if statements ## Configuration @@ -710,6 +712,9 @@ linters-settings: gocognit: # minimal code complexity to report, 30 by default (but we recommend 10-20) min-complexity: 10 + nestif: + # minimal complexity of if statements to report, 5 by default + min-complexity: 4 goconst: # minimal length of string constant, 3 by default min-len: 3 @@ -777,8 +782,8 @@ linters-settings: modules: # List of blocked modules # - github.com/uudashr/go-module: # Blocked module # recommendations: # Recommended modules that should be used instead (Optional) - # - golang.org/x/mod - # reason: "`mod` is the official go.mod parser library." # Reason why the recommended module should be used (Optional) + # - golang.org/x/mod + # reason: "`mod` is the official go.mod parser library." # Reason why the recommended module should be used (Optional) govet: # report about shadowed variables check-shadowing: true @@ -1288,6 +1293,7 @@ Thanks to developers and authors of used linters: - [ryancurrah](https://github.com/ryancurrah) - [tetafro](https://github.com/tetafro) - [maratori](https://github.com/maratori) +- [nakabonne](https://github.com/nakabonne) ## Changelog diff --git a/go.mod b/go.mod index d9512f62..31dc9ad6 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( github.com/mattn/go-colorable v0.1.4 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b + github.com/nakabonne/nestif v0.3.0 github.com/pkg/errors v0.8.1 github.com/ryancurrah/gomodguard v1.0.2 github.com/securego/gosec v0.0.0-20200103095621-79fbf3af8d83 diff --git a/go.sum b/go.sum index 2e881724..35bafeb4 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,8 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mozilla/tls-observatory v0.0.0-20190404164649-a3c1b6cfecfd/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nakabonne/nestif v0.3.0 h1:+yOViDGhg8ygGrmII72nV9B/zGxY188TYpfolntsaPw= +github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d h1:AREM5mwr4u1ORQBMvzfzBgpsctsbQikCVpvC+tX285E= github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= diff --git a/pkg/config/config.go b/pkg/config/config.go index 0e55666a..f97ec591 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -212,6 +212,7 @@ type LintersSettings struct { Gocognit GocognitSettings Godot GodotSettings Testpackage TestpackageSettings + Nestif NestifSettings Custom map[string]CustomLinterSettings } @@ -297,6 +298,10 @@ type TestpackageSettings struct { SkipRegexp string `mapstructure:"skip-regexp"` } +type NestifSettings struct { + MinComplexity int `mapstructure:"min-complexity"` +} + //nolint:gomnd var defaultLintersSettings = LintersSettings{ Lll: LllSettings{ @@ -339,6 +344,9 @@ var defaultLintersSettings = LintersSettings{ Testpackage: TestpackageSettings{ SkipRegexp: `(export|internal)_test\.go`, }, + Nestif: NestifSettings{ + MinComplexity: 5, + }, } type CustomLinterSettings struct { diff --git a/pkg/golinters/nestif.go b/pkg/golinters/nestif.go new file mode 100644 index 00000000..841a22d4 --- /dev/null +++ b/pkg/golinters/nestif.go @@ -0,0 +1,65 @@ +package golinters + +import ( + "sort" + "sync" + + "github.com/nakabonne/nestif" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" + "github.com/golangci/golangci-lint/pkg/lint/linter" + "github.com/golangci/golangci-lint/pkg/result" +) + +const nestifName = "nestif" + +func NewNestif() *goanalysis.Linter { + var mu sync.Mutex + var resIssues []goanalysis.Issue + + analyzer := &analysis.Analyzer{ + Name: goanalysis.TheOnlyAnalyzerName, + Doc: goanalysis.TheOnlyanalyzerDoc, + } + return goanalysis.NewLinter( + nestifName, + "Reports deeply nested if statements", + []*analysis.Analyzer{analyzer}, + nil, + ).WithContextSetter(func(lintCtx *linter.Context) { + analyzer.Run = func(pass *analysis.Pass) (interface{}, error) { + checker := &nestif.Checker{ + MinComplexity: lintCtx.Settings().Nestif.MinComplexity, + } + var issues []nestif.Issue + for _, f := range pass.Files { + issues = append(issues, checker.Check(f, pass.Fset)...) + } + if len(issues) == 0 { + return nil, nil + } + + sort.SliceStable(issues, func(i, j int) bool { + return issues[i].Complexity > issues[j].Complexity + }) + + res := make([]goanalysis.Issue, 0, len(issues)) + for _, i := range issues { + res = append(res, goanalysis.NewIssue(&result.Issue{ //nolint:scopelint + Pos: i.Pos, + Text: i.Message, + FromLinter: nestifName, + }, pass)) + } + + mu.Lock() + resIssues = append(resIssues, res...) + mu.Unlock() + + return nil, nil + } + }).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { + return resIssues + }).WithLoadMode(goanalysis.LoadModeSyntax) +} diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index a3e84f0c..24ac59c6 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -260,6 +260,9 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config { WithPresets(linter.PresetStyle). WithLoadForGoAnalysis(). WithURL("https://github.com/maratori/testpackage"), + linter.NewConfig(golinters.NewNestif()). + WithPresets(linter.PresetComplexity). + WithURL("https://github.com/nakabonne/nestif"), } isLocalRun := os.Getenv("GOLANGCI_COM_RUN") == "" diff --git a/test/testdata/nestif.go b/test/testdata/nestif.go new file mode 100644 index 00000000..350e0a72 --- /dev/null +++ b/test/testdata/nestif.go @@ -0,0 +1,47 @@ +//args: -Enestif +//config: linters-settings.nestif.min-complexity=1 +package testdata + +func _() { + var b1, b2, b3, b4 bool + + if b1 { // ERROR "`if b1` is deeply nested \(complexity: 1\)" + if b2 { // +1 + } + } + + if b1 { // ERROR "`if b1` is deeply nested \(complexity: 3\)" + if b2 { // +1 + if b3 { // +2 + } + } + } + + if b1 { // ERROR "`if b1` is deeply nested \(complexity: 5\)" + if b2 { // +1 + } else if b3 { // +1 + if b4 { // +2 + } + } else { // +1 + } + } + + if b1 { // ERROR "`if b1` is deeply nested \(complexity: 9\)" + if b2 { // +1 + if b3 { // +2 + } + } + + if b2 { // +1 + if b3 { // +2 + if b4 { // +3 + } + } + } + } + + if b1 == b2 == b3 { // ERROR "`if b1 == b2 == b3` is deeply nested \(complexity: 1\)" + if b4 { // +1 + } + } +}