2018-01-27 05:48:44 +02:00
|
|
|
package rule
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"go/ast"
|
|
|
|
"go/token"
|
2022-04-10 09:06:59 +02:00
|
|
|
"sync"
|
2018-01-27 05:48:44 +02:00
|
|
|
|
|
|
|
"github.com/mgechev/revive/lint"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Based on https://github.com/fzipp/gocyclo
|
|
|
|
|
|
|
|
// CyclomaticRule lints given else constructs.
|
2021-10-17 20:34:48 +02:00
|
|
|
type CyclomaticRule struct {
|
|
|
|
maxComplexity int
|
2022-04-10 09:06:59 +02:00
|
|
|
sync.Mutex
|
2021-10-17 20:34:48 +02:00
|
|
|
}
|
2018-01-27 05:48:44 +02:00
|
|
|
|
2023-05-20 14:44:34 +02:00
|
|
|
const defaultMaxCyclomaticComplexity = 10
|
|
|
|
|
2022-04-10 09:06:59 +02:00
|
|
|
func (r *CyclomaticRule) configure(arguments lint.Arguments) {
|
|
|
|
r.Lock()
|
2023-05-20 14:44:34 +02:00
|
|
|
defer r.Unlock()
|
2024-10-01 12:14:02 +02:00
|
|
|
if r.maxComplexity != 0 {
|
|
|
|
return // already configured
|
|
|
|
}
|
2018-01-27 05:48:44 +02:00
|
|
|
|
2024-10-01 12:14:02 +02:00
|
|
|
if len(arguments) < 1 {
|
|
|
|
r.maxComplexity = defaultMaxCyclomaticComplexity
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
complexity, ok := arguments[0].(int64) // Alt. non panicking version
|
|
|
|
if !ok {
|
|
|
|
panic(fmt.Sprintf("invalid argument for cyclomatic complexity; expected int but got %T", arguments[0]))
|
2018-01-27 05:48:44 +02:00
|
|
|
}
|
2024-10-01 12:14:02 +02:00
|
|
|
r.maxComplexity = int(complexity)
|
2022-04-10 09:06:59 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Apply applies the rule to given file.
|
|
|
|
func (r *CyclomaticRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
|
|
|
|
r.configure(arguments)
|
2018-01-27 05:48:44 +02:00
|
|
|
|
2021-08-17 21:14:42 +02:00
|
|
|
var failures []lint.Failure
|
2018-01-27 05:48:44 +02:00
|
|
|
fileAst := file.AST
|
2022-04-10 09:06:59 +02:00
|
|
|
|
2018-01-27 05:48:44 +02:00
|
|
|
walker := lintCyclomatic{
|
|
|
|
file: file,
|
2021-10-17 20:34:48 +02:00
|
|
|
complexity: r.maxComplexity,
|
2018-01-27 05:48:44 +02:00
|
|
|
onFailure: func(failure lint.Failure) {
|
|
|
|
failures = append(failures, failure)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
ast.Walk(walker, fileAst)
|
|
|
|
|
|
|
|
return failures
|
|
|
|
}
|
|
|
|
|
|
|
|
// Name returns the rule name.
|
2022-04-10 11:55:13 +02:00
|
|
|
func (*CyclomaticRule) Name() string {
|
2018-01-27 06:45:17 +02:00
|
|
|
return "cyclomatic"
|
2018-01-27 05:48:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type lintCyclomatic struct {
|
|
|
|
file *lint.File
|
|
|
|
complexity int
|
|
|
|
onFailure func(lint.Failure)
|
|
|
|
}
|
|
|
|
|
2018-09-23 00:27:22 +02:00
|
|
|
func (w lintCyclomatic) Visit(_ ast.Node) ast.Visitor {
|
2018-01-27 05:48:44 +02:00
|
|
|
f := w.file
|
|
|
|
for _, decl := range f.AST.Decls {
|
2024-10-01 12:14:02 +02:00
|
|
|
fn, ok := decl.(*ast.FuncDecl)
|
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
c := complexity(fn)
|
|
|
|
if c > w.complexity {
|
|
|
|
w.onFailure(lint.Failure{
|
|
|
|
Confidence: 1,
|
|
|
|
Category: "maintenance",
|
|
|
|
Failure: fmt.Sprintf("function %s has cyclomatic complexity %d (> max enabled %d)",
|
|
|
|
funcName(fn), c, w.complexity),
|
|
|
|
Node: fn,
|
|
|
|
})
|
2018-01-27 05:48:44 +02:00
|
|
|
}
|
|
|
|
}
|
2024-10-01 12:14:02 +02:00
|
|
|
|
2018-01-27 05:48:44 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2024-10-01 12:14:02 +02:00
|
|
|
declarationHasReceiver := fn.Recv != nil && fn.Recv.NumFields() > 0
|
|
|
|
if declarationHasReceiver {
|
|
|
|
typ := fn.Recv.List[0].Type
|
|
|
|
return fmt.Sprintf("(%s).%s", recvString(typ), fn.Name)
|
2018-01-27 05:48:44 +02:00
|
|
|
}
|
2024-10-01 12:14:02 +02:00
|
|
|
|
2018-01-27 05:48:44 +02:00
|
|
|
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
|
|
|
|
}
|