225 lines
5.2 KiB
Go
225 lines
5.2 KiB
Go
// 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 [<flag> ...] <Go file or directory> ...
|
|
//
|
|
// 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:
|
|
// <complexity> <package> <function> <file:row:column>
|
|
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
const usageDoc = `Calculate cyclomatic complexities of Go functions.
|
|
Usage:
|
|
gocyclo [flags] <Go file or directory> ...
|
|
|
|
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:
|
|
<complexity> <package> <function> <file:row:column>
|
|
`
|
|
|
|
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 main() {
|
|
log.SetFlags(0)
|
|
log.SetPrefix("gocyclo: ")
|
|
flag.Usage = usage
|
|
flag.Parse()
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
usage()
|
|
}
|
|
|
|
stats := analyze(args)
|
|
sort.Sort(byComplexity(stats))
|
|
written := writeStats(os.Stdout, stats)
|
|
|
|
if *avg {
|
|
showAverage(stats)
|
|
}
|
|
|
|
if *over > 0 && written > 0 {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
log.Fatal(err)
|
|
}
|
|
return buildStats(f, fset, stats)
|
|
}
|
|
|
|
func analyzeDir(dirname string, stats []stat) []stat {
|
|
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
|
|
if err == nil && !info.IsDir() && strings.HasSuffix(path, ".go") {
|
|
stats = analyzeFile(path, stats)
|
|
}
|
|
return err
|
|
})
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|