diff --git a/.golangci.example.yml b/.golangci.example.yml index 2a0a3dd5..dffaf9e2 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -125,6 +125,9 @@ linters-settings: # if it's called for subdir of a project it can't find external interfaces. All text editor integrations # with golangci-lint call it on a directory with the changed file. check-exported: false + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 30 linters: diff --git a/README.md b/README.md index ac2b79e3..1ff1dcd5 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ depguard: Go linter that checks if package imports are in a list of acceptable p misspell: Finds commonly misspelled English words in comments [fast: true] lll: Reports long lines [fast: true] unparam: Reports unused function parameters [fast: false] +nakedret: Finds naked returns in functions greater than a specified function length [fast: true] ``` Pass `-E/--enable` to enable linter and `-D/--disable` to disable: @@ -231,6 +232,7 @@ golangci-lint linters - [misspell](https://github.com/client9/misspell) - Finds commonly misspelled English words in comments - [lll](https://github.com/walle/lll) - Reports long lines - [unparam](https://github.com/mvdan/unparam) - Reports unused function parameters +- [nakedret](https://github.com/alexkohler/nakedret) - Finds naked returns in functions greater than a specified function length # Configuration The config file has lower priority than command-line options. If the same bool/string/int option is provided on the command-line @@ -454,6 +456,9 @@ linters-settings: # if it's called for subdir of a project it can't find external interfaces. All text editor integrations # with golangci-lint call it on a directory with the changed file. check-exported: false + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 30 linters: @@ -636,6 +641,7 @@ Thanks to developers and authors of used linters: - [OpenPeeDeeP](https://github.com/OpenPeeDeeP) - [client9](https://github.com/client9) - [walle](https://github.com/walle) +- [alexkohler](https://github.com/alexkohler) # Future Plans 1. Upstream all changes of forked linters. diff --git a/pkg/config/config.go b/pkg/config/config.go index 70613451..a4afa4ce 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -160,14 +160,24 @@ type LintersSettings struct { CheckExported bool `mapstructure:"check-exported"` } - Lll LllSettings - Unparam UnparamSettings + Lll LllSettings + Unparam UnparamSettings + Nakedret NakedretSettings } type LllSettings struct { LineLength int `mapstructure:"line-length"` } +type NakedretSettings struct { + MaxFuncLines int `mapstructure:"max-func-lines"` +} + +type UnparamSettings struct { + CheckExported bool `mapstructure:"check-exported"` + Algo string +} + var defaultLintersSettings = LintersSettings{ Lll: LllSettings{ LineLength: 120, @@ -175,11 +185,9 @@ var defaultLintersSettings = LintersSettings{ Unparam: UnparamSettings{ Algo: "cha", }, -} - -type UnparamSettings struct { - CheckExported bool `mapstructure:"check-exported"` - Algo string + Nakedret: NakedretSettings{ + MaxFuncLines: 30, + }, } type Linters struct { diff --git a/pkg/golinters/nakedret.go b/pkg/golinters/nakedret.go new file mode 100644 index 00000000..b935faa7 --- /dev/null +++ b/pkg/golinters/nakedret.go @@ -0,0 +1,100 @@ +package golinters + +import ( + "context" + "fmt" + "go/ast" + "go/token" + + "github.com/golangci/golangci-lint/pkg/lint/linter" + "github.com/golangci/golangci-lint/pkg/result" +) + +type Nakedret struct{} + +func (Nakedret) Name() string { + return "nakedret" +} + +func (Nakedret) Desc() string { + return "Finds naked returns in functions greater than a specified function length" +} + +type nakedretVisitor struct { + maxLength int + f *token.FileSet + issues []result.Issue +} + +func (v *nakedretVisitor) processFuncDecl(funcDecl *ast.FuncDecl) { + file := v.f.File(funcDecl.Pos()) + functionLineLength := file.Position(funcDecl.End()).Line - file.Position(funcDecl.Pos()).Line + + // Scan the body for usage of the named returns + for _, stmt := range funcDecl.Body.List { + s, ok := stmt.(*ast.ReturnStmt) + if !ok { + continue + } + + if len(s.Results) != 0 { + continue + } + + file := v.f.File(s.Pos()) + if file == nil || functionLineLength <= v.maxLength { + continue + } + if funcDecl.Name == nil { + continue + } + + v.issues = append(v.issues, result.Issue{ + FromLinter: Nakedret{}.Name(), + Text: fmt.Sprintf("naked return in func `%s` with %d lines of code", + funcDecl.Name.Name, functionLineLength), + Pos: v.f.Position(s.Pos()), + }) + } +} + +func (v *nakedretVisitor) Visit(node ast.Node) ast.Visitor { + funcDecl, ok := node.(*ast.FuncDecl) + if !ok { + return v + } + + var namedReturns []*ast.Ident + + // We've found a function + if funcDecl.Type != nil && funcDecl.Type.Results != nil { + for _, field := range funcDecl.Type.Results.List { + for _, ident := range field.Names { + if ident != nil { + namedReturns = append(namedReturns, ident) + } + } + } + } + + if len(namedReturns) == 0 || funcDecl.Body == nil { + return v + } + + v.processFuncDecl(funcDecl) + return v +} + +func (lint Nakedret) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) { + var res []result.Issue + for _, f := range lintCtx.ASTCache.GetAllValidFiles() { + v := nakedretVisitor{ + maxLength: lintCtx.Settings().Nakedret.MaxFuncLines, + f: f.Fset, + } + ast.Walk(&v, f.F) + res = append(res, v.issues...) + } + + return res, nil +} diff --git a/pkg/lint/lintersdb/lintersdb.go b/pkg/lint/lintersdb/lintersdb.go index e4f06e60..3f38ebce 100644 --- a/pkg/lint/lintersdb/lintersdb.go +++ b/pkg/lint/lintersdb/lintersdb.go @@ -177,6 +177,10 @@ func GetAllSupportedLinterConfigs() []linter.Config { WithFullImport(). WithSSA(). WithURL("https://github.com/mvdan/unparam"), + linter.NewConfig(golinters.Nakedret{}). + WithPresets(linter.PresetComplexity). + WithSpeed(10). + WithURL("https://github.com/alexkohler/nakedret"), } if os.Getenv("GOLANGCI_COM_RUN") == "1" { @@ -188,6 +192,7 @@ func GetAllSupportedLinterConfigs() []linter.Config { golinters.Misspell{}.Name(): true, // unsure about false-positives number golinters.Lll{}.Name(): true, // annoying golinters.Unparam{}.Name(): true, // need to check it first + golinters.Nakedret{}.Name(): true, // annoying } return enableLinterConfigs(lcs, func(lc *linter.Config) bool { return !disabled[lc.Linter.Name()] diff --git a/test/testdata/nakedret.go b/test/testdata/nakedret.go new file mode 100644 index 00000000..3b78c823 --- /dev/null +++ b/test/testdata/nakedret.go @@ -0,0 +1,67 @@ +//args: -Enakedret +package testdata + +func NakedretIssue() (a int, b string) { + if a > 0 { + return + } + + if b == "" { + return 0, "0" + } + + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + + // len of this function is 31 + return // ERROR "naked return in func `NakedretIssue` with 31 lines of code" +} + +func NoNakedretIssue() (a int, b string) { + if a > 0 { + return + } + + if b == "" { + return 0, "0" + } + + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + // ... + + // len of this function is 30 + return +} diff --git a/third_party/nakedret/LICENSE b/third_party/nakedret/LICENSE new file mode 100644 index 00000000..d3191917 --- /dev/null +++ b/third_party/nakedret/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Alex Kohler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file