diff --git a/Gopkg.lock b/Gopkg.lock index 93fe1c20..66d55f1f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -64,6 +64,12 @@ packages = ["deadcode"] revision = "a82b63c685e730fbc1efbade9ce6316ac85cceb7" +[[projects]] + branch = "master" + name = "github.com/golangci/gocyclo" + packages = ["."] + revision = "687488c898ab673ee751943f7bcab53ce2371985" + [[projects]] branch = "master" name = "github.com/golangci/gofmt" @@ -237,6 +243,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "fd69e23afecb188e33d824ca597d116e47e913418eb1f0842ccb4f9bb44c7c7c" + inputs-digest = "27116578a35d2f40d043e02943a6f64062ec09365a37e5b10a6d8c7710461bf8" solver-name = "gps-cdcl" solver-version = 1 diff --git a/internal/commands/run.go b/internal/commands/run.go index 70362ef7..c4c5702e 100644 --- a/internal/commands/run.go +++ b/internal/commands/run.go @@ -49,6 +49,9 @@ func (e *Executor) initRun() { runCmd.Flags().BoolVar(&rc.Gofmt.Simplify, "gofmt.simplify", true, "Gofmt: simplify code") + runCmd.Flags().IntVar(&rc.Gocyclo.MinComplexity, "gocyclo.min-complexity", + 20, "Minimal complexity of function to report it") + runCmd.Flags().StringSliceVarP(&rc.EnabledLinters, "enable", "E", []string{}, "Enable specific linter") runCmd.Flags().StringSliceVarP(&rc.DisabledLinters, "disable", "D", []string{}, "Disable specific linter") runCmd.Flags().BoolVar(&rc.EnableAllLinters, "enable-all", false, "Enable all linters") @@ -94,8 +97,8 @@ func (e Executor) executeRun(cmd *cobra.Command, args []string) { runner := pkg.SimpleRunner{ Processors: []processors.Processor{ processors.MaxLinterIssuesPerFile{}, - processors.UniqByLineProcessor{}, processors.NewExcludeProcessor(fmt.Sprintf("(%s)", strings.Join(e.cfg.Run.ExcludePatterns, "|"))), + processors.UniqByLineProcessor{}, processors.NewPathPrettifier(), }, } diff --git a/pkg/config/config.go b/pkg/config/config.go index 9afdf2c4..5fd6e116 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -41,6 +41,9 @@ type Run struct { Gofmt struct { Simplify bool } + Gocyclo struct { + MinComplexity int + } EnabledLinters []string DisabledLinters []string diff --git a/pkg/golinters/enabled_linters.go b/pkg/golinters/enabled_linters.go index 72f131fe..fe38e415 100644 --- a/pkg/golinters/enabled_linters.go +++ b/pkg/golinters/enabled_linters.go @@ -38,6 +38,7 @@ func GetAllSupportedLinterConfigs() []LinterConfig { enabledByDefault(errcheck{}, "Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases"), enabledByDefault(golint{}, "Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes"), enabledByDefault(deadcode{}, "Finds unused code"), + enabledByDefault(gocyclo{}, "Computes and checks the cyclomatic complexity of functions"), disabledByDefault(gofmt{}, "Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification"), disabledByDefault(gofmt{useGoimports: true}, "Goimports does everything that gofmt does. Additionally it checks unused imports"), diff --git a/pkg/golinters/gocyclo.go b/pkg/golinters/gocyclo.go new file mode 100644 index 00000000..4c73d828 --- /dev/null +++ b/pkg/golinters/gocyclo.go @@ -0,0 +1,38 @@ +package golinters + +import ( + "context" + "fmt" + + gocycloAPI "github.com/golangci/gocyclo" + "github.com/golangci/golangci-lint/pkg/config" + "github.com/golangci/golangci-lint/pkg/result" + "github.com/golangci/golangci-shared/pkg/executors" +) + +type gocyclo struct{} + +func (gocyclo) Name() string { + return "gocyclo" +} + +func (g gocyclo) Run(ctx context.Context, exec executors.Executor, cfg *config.Run) (*result.Result, error) { + stats := gocycloAPI.Run(cfg.Paths.MixedPaths()) + + res := &result.Result{} + for _, s := range stats { + if s.Complexity < cfg.Gocyclo.MinComplexity { + continue + } + + res.Issues = append(res.Issues, result.Issue{ + File: s.Pos.Filename, + LineNumber: s.Pos.Line, + Text: fmt.Sprintf("cyclomatic complexity %d of func %s is high (> %d)", + s.Complexity, formatCode(s.FuncName, cfg), cfg.Gocyclo.MinComplexity), + FromLinter: g.Name(), + }) + } + + return res, nil +} diff --git a/pkg/golinters/testdata/gocyclo.go b/pkg/golinters/testdata/gocyclo.go new file mode 100644 index 00000000..e0775031 --- /dev/null +++ b/pkg/golinters/testdata/gocyclo.go @@ -0,0 +1,15 @@ +package testdata + +func GocycloBigComplexity(s string) { // ERROR "cyclomatic complexity .* of func .* is high .*" + if s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" { + return + } + + if s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" { + return + } + + if s == "1" || s == "2" || s == "3" || s == "4" || s == "5" || s == "6" || s == "7" { + return + } +} diff --git a/vendor/github.com/golangci/gocyclo/CONTRIBUTORS b/vendor/github.com/golangci/gocyclo/CONTRIBUTORS new file mode 100644 index 00000000..1c09f1a0 --- /dev/null +++ b/vendor/github.com/golangci/gocyclo/CONTRIBUTORS @@ -0,0 +1,7 @@ +# Names should be added to this file like so: +# Name + +# Please keep the list sorted. + +Frederik Zipp +Harshavardhana diff --git a/vendor/github.com/golangci/gocyclo/LICENSE b/vendor/github.com/golangci/gocyclo/LICENSE new file mode 100644 index 00000000..45f88d6c --- /dev/null +++ b/vendor/github.com/golangci/gocyclo/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013 Frederik Zipp. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of the copyright owner nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/golangci/gocyclo/README.md b/vendor/github.com/golangci/gocyclo/README.md new file mode 100644 index 00000000..b2ec36a7 --- /dev/null +++ b/vendor/github.com/golangci/gocyclo/README.md @@ -0,0 +1,31 @@ +Gocyclo calculates cyclomatic complexities of functions in Go source code. + +The cyclomatic complexity of a function is calculated according to the +following rules: + + 1 is the base complexity of a function + +1 for each 'if', 'for', 'case', '&&' or '||' + +To install, run + + $ go get github.com/fzipp/gocyclo + +and put the resulting binary in one of your PATH directories if +`$GOPATH/bin` isn't already in your PATH. + +Usage: + + $ gocyclo [ ...] ... + +Examples: + + $ gocyclo . + $ gocyclo main.go + $ gocyclo -top 10 src/ + $ gocyclo -over 25 docker + $ gocyclo -avg . + +The output fields for each line are: + + + diff --git a/vendor/github.com/golangci/gocyclo/gocyclo.go b/vendor/github.com/golangci/gocyclo/gocyclo.go new file mode 100644 index 00000000..c376f21e --- /dev/null +++ b/vendor/github.com/golangci/gocyclo/gocyclo.go @@ -0,0 +1,212 @@ +// Copyright 2013 Frederik Zipp. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Gocyclo calculates the cyclomatic complexities of functions and +// methods in Go source code. +// +// Usage: +// gocyclo [ ...] ... +// +// Flags: +// -over N show functions with complexity > N only and +// return exit code 1 if the output is non-empty +// -top N show the top N most complex functions only +// -avg show the average complexity +// +// The output fields for each line are: +// +package gocyclo + +import ( + "flag" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "os" + "path/filepath" + "sort" +) + +const usageDoc = `Calculate cyclomatic complexities of Go functions. +Usage: + gocyclo [flags] ... + +Flags: + -over N show functions with complexity > N only and + return exit code 1 if the set is non-empty + -top N show the top N most complex functions only + -avg show the average complexity over all functions, + not depending on whether -over or -top are set + +The output fields for each line are: + +` + +func usage() { + fmt.Fprintf(os.Stderr, usageDoc) + os.Exit(2) +} + +var ( + over = flag.Int("over", 0, "show functions with complexity > N only") + top = flag.Int("top", -1, "show the top N most complex functions only") + avg = flag.Bool("avg", false, "show the average complexity") +) + +func Run(paths []string) []Stat { + stats := analyze(paths) + sort.Sort(byComplexity(stats)) + + var retStats []Stat + for _, s := range stats { + retStats = append(retStats, Stat(s)) + } + + return retStats +} + +func analyze(paths []string) []stat { + var stats []stat + for _, path := range paths { + if isDir(path) { + stats = analyzeDir(path, stats) + } else { + stats = analyzeFile(path, stats) + } + } + return stats +} + +func isDir(filename string) bool { + fi, err := os.Stat(filename) + return err == nil && fi.IsDir() +} + +func analyzeFile(fname string, stats []stat) []stat { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, fname, nil, 0) + if err != nil { + panic(err) + } + return buildStats(f, fset, stats) +} + +func analyzeDir(dirname string, stats []stat) []stat { + files, _ := filepath.Glob(filepath.Join(dirname, "*.go")) + for _, f := range files { + stats = analyzeFile(f, stats) + } + return stats +} + +func writeStats(w io.Writer, sortedStats []stat) int { + for i, stat := range sortedStats { + if i == *top { + return i + } + if stat.Complexity <= *over { + return i + } + fmt.Fprintln(w, stat) + } + return len(sortedStats) +} + +func showAverage(stats []stat) { + fmt.Printf("Average: %.3g\n", average(stats)) +} + +func average(stats []stat) float64 { + total := 0 + for _, s := range stats { + total += s.Complexity + } + return float64(total) / float64(len(stats)) +} + +type stat struct { + PkgName string + FuncName string + Complexity int + Pos token.Position +} + +type Stat stat + +func (s stat) String() string { + return fmt.Sprintf("%d %s %s %s", s.Complexity, s.PkgName, s.FuncName, s.Pos) +} + +type byComplexity []stat + +func (s byComplexity) Len() int { return len(s) } +func (s byComplexity) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byComplexity) Less(i, j int) bool { + return s[i].Complexity >= s[j].Complexity +} + +func buildStats(f *ast.File, fset *token.FileSet, stats []stat) []stat { + for _, decl := range f.Decls { + if fn, ok := decl.(*ast.FuncDecl); ok { + stats = append(stats, stat{ + PkgName: f.Name.Name, + FuncName: funcName(fn), + Complexity: complexity(fn), + Pos: fset.Position(fn.Pos()), + }) + } + } + return stats +} + +// funcName returns the name representation of a function or method: +// "(Type).Name" for methods or simply "Name" for functions. +func funcName(fn *ast.FuncDecl) string { + if fn.Recv != nil { + if fn.Recv.NumFields() > 0 { + typ := fn.Recv.List[0].Type + return fmt.Sprintf("(%s).%s", recvString(typ), fn.Name) + } + } + return fn.Name.Name +} + +// recvString returns a string representation of recv of the +// form "T", "*T", or "BADRECV" (if not a proper receiver type). +func recvString(recv ast.Expr) string { + switch t := recv.(type) { + case *ast.Ident: + return t.Name + case *ast.StarExpr: + return "*" + recvString(t.X) + } + return "BADRECV" +} + +// complexity calculates the cyclomatic complexity of a function. +func complexity(fn *ast.FuncDecl) int { + v := complexityVisitor{} + ast.Walk(&v, fn) + return v.Complexity +} + +type complexityVisitor struct { + // Complexity is the cyclomatic complexity + Complexity int +} + +// Visit implements the ast.Visitor interface. +func (v *complexityVisitor) Visit(n ast.Node) ast.Visitor { + switch n := n.(type) { + case *ast.FuncDecl, *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.CaseClause, *ast.CommClause: + v.Complexity++ + case *ast.BinaryExpr: + if n.Op == token.LAND || n.Op == token.LOR { + v.Complexity++ + } + } + return v +}