1
0
mirror of https://github.com/mgechev/revive.git synced 2024-12-12 10:44:59 +02:00
revive/rule/cognitive_complexity.go

220 lines
5.0 KiB
Go
Raw Normal View History

2019-12-14 09:27:06 +02:00
package rule
import (
"fmt"
"go/ast"
"go/token"
2022-04-10 09:06:59 +02:00
"sync"
2019-12-14 09:27:06 +02:00
"github.com/mgechev/revive/lint"
"golang.org/x/tools/go/ast/astutil"
2019-12-14 09:27:06 +02:00
)
2024-12-01 17:44:41 +02:00
// CognitiveComplexityRule sets restriction for maximum cognitive complexity.
2021-10-17 20:34:48 +02:00
type CognitiveComplexityRule struct {
maxComplexity int
configureOnce sync.Once
2021-10-17 20:34:48 +02:00
}
2019-12-14 09:27:06 +02:00
const defaultMaxCognitiveComplexity = 7
2022-04-10 09:06:59 +02:00
func (r *CognitiveComplexityRule) configure(arguments lint.Arguments) {
2024-10-01 12:14:02 +02:00
if len(arguments) < 1 {
r.maxComplexity = defaultMaxCognitiveComplexity
return
}
complexity, ok := arguments[0].(int64)
if !ok {
panic(fmt.Sprintf("invalid argument type for cognitive-complexity, expected int64, got %T", arguments[0]))
}
r.maxComplexity = int(complexity)
2022-04-10 09:06:59 +02:00
}
// Apply applies the rule to given file.
func (r *CognitiveComplexityRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
r.configureOnce.Do(func() { r.configure(arguments) })
2019-12-14 09:27:06 +02:00
var failures []lint.Failure
2022-04-10 09:06:59 +02:00
2019-12-14 09:27:06 +02:00
linter := cognitiveComplexityLinter{
file: file,
2021-10-17 20:34:48 +02:00
maxComplexity: r.maxComplexity,
2019-12-14 09:27:06 +02:00
onFailure: func(failure lint.Failure) {
failures = append(failures, failure)
},
}
2022-04-10 11:55:13 +02:00
linter.lintCognitiveComplexity()
2019-12-14 09:27:06 +02:00
return failures
}
// Name returns the rule name.
2022-04-10 11:55:13 +02:00
func (*CognitiveComplexityRule) Name() string {
2019-12-14 09:27:06 +02:00
return "cognitive-complexity"
}
type cognitiveComplexityLinter struct {
file *lint.File
maxComplexity int
onFailure func(lint.Failure)
}
2022-04-10 11:55:13 +02:00
func (w cognitiveComplexityLinter) lintCognitiveComplexity() {
2019-12-14 09:27:06 +02:00
f := w.file
for _, decl := range f.AST.Decls {
2020-08-05 18:25:57 +02:00
if fn, ok := decl.(*ast.FuncDecl); ok && fn.Body != nil {
v := cognitiveComplexityVisitor{
name: fn.Name,
}
2019-12-14 09:27:06 +02:00
c := v.subTreeComplexity(fn.Body)
if c > w.maxComplexity {
w.onFailure(lint.Failure{
Confidence: 1,
Category: "maintenance",
Failure: fmt.Sprintf("function %s has cognitive complexity %d (> max enabled %d)", funcName(fn), c, w.maxComplexity),
Node: fn,
})
}
}
}
}
type cognitiveComplexityVisitor struct {
name *ast.Ident
2019-12-14 09:27:06 +02:00
complexity int
nestingLevel int
}
2019-12-14 10:12:36 +02:00
// subTreeComplexity calculates the cognitive complexity of an AST-subtree.
2019-12-14 09:27:06 +02:00
func (v cognitiveComplexityVisitor) subTreeComplexity(n ast.Node) int {
ast.Walk(&v, n)
return v.complexity
}
// Visit implements the ast.Visitor interface.
func (v *cognitiveComplexityVisitor) Visit(n ast.Node) ast.Visitor {
switch n := n.(type) {
case *ast.IfStmt:
targets := []ast.Node{n.Cond, n.Body, n.Else}
v.walk(1, targets...)
return nil
case *ast.ForStmt:
targets := []ast.Node{n.Cond, n.Body}
v.walk(1, targets...)
return nil
case *ast.RangeStmt:
v.walk(1, n.Body)
return nil
case *ast.SelectStmt:
v.walk(1, n.Body)
return nil
case *ast.SwitchStmt:
v.walk(1, n.Body)
return nil
case *ast.TypeSwitchStmt:
v.walk(1, n.Body)
return nil
case *ast.FuncLit:
v.walk(0, n.Body) // do not increment the complexity, just do the nesting
return nil
case *ast.BinaryExpr:
v.complexity += v.binExpComplexity(n)
return nil // skip visiting binexp sub-tree (already visited by binExpComplexity)
case *ast.BranchStmt:
if n.Label != nil {
v.complexity++
2019-12-14 09:27:06 +02:00
}
case *ast.CallExpr:
if ident, ok := n.Fun.(*ast.Ident); ok {
if ident.Obj == v.name.Obj && ident.Name == v.name.Name {
// called by same function directly (direct recursion)
v.complexity++
return nil
}
}
2019-12-14 09:27:06 +02:00
}
return v
}
func (v *cognitiveComplexityVisitor) walk(complexityIncrement int, targets ...ast.Node) {
v.complexity += complexityIncrement + v.nestingLevel
nesting := v.nestingLevel
v.nestingLevel++
for _, t := range targets {
if t == nil {
continue
}
ast.Walk(v, t)
}
v.nestingLevel = nesting
}
func (cognitiveComplexityVisitor) binExpComplexity(n *ast.BinaryExpr) int {
calculator := binExprComplexityCalculator{opsStack: []token.Token{}}
astutil.Apply(n, calculator.pre, calculator.post)
2019-12-14 09:27:06 +02:00
return calculator.complexity
}
type binExprComplexityCalculator struct {
complexity int
opsStack []token.Token // stack of bool operators
subexpStarted bool
2019-12-14 09:27:06 +02:00
}
func (becc *binExprComplexityCalculator) pre(c *astutil.Cursor) bool {
switch n := c.Node().(type) {
case *ast.BinaryExpr:
isBoolOp := n.Op == token.LAND || n.Op == token.LOR
if !isBoolOp {
break
}
ops := len(becc.opsStack)
// if
// is the first boolop in the expression OR
// is the first boolop inside a subexpression (...) OR
// is not the same to the previous one
// then
// increment complexity
if ops == 0 || becc.subexpStarted || n.Op != becc.opsStack[ops-1] {
becc.complexity++
becc.subexpStarted = false
2019-12-14 09:27:06 +02:00
}
becc.opsStack = append(becc.opsStack, n.Op)
case *ast.ParenExpr:
becc.subexpStarted = true
2019-12-14 09:27:06 +02:00
}
return true
}
2019-12-14 09:27:06 +02:00
func (becc *binExprComplexityCalculator) post(c *astutil.Cursor) bool {
switch n := c.Node().(type) {
case *ast.BinaryExpr:
isBoolOp := n.Op == token.LAND || n.Op == token.LOR
if !isBoolOp {
break
}
ops := len(becc.opsStack)
if ops > 0 {
becc.opsStack = becc.opsStack[:ops-1]
}
case *ast.ParenExpr:
becc.subexpStarted = false
}
return true
2019-12-14 09:27:06 +02:00
}