196 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			196 lines
		
	
	
		
			4.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package rule
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"go/ast"
 | |
| 	"go/token"
 | |
| 
 | |
| 	"github.com/mgechev/revive/lint"
 | |
| 	"golang.org/x/tools/go/ast/astutil"
 | |
| )
 | |
| 
 | |
| // CognitiveComplexityRule lints given else constructs.
 | |
| type CognitiveComplexityRule struct{}
 | |
| 
 | |
| // Apply applies the rule to given file.
 | |
| func (r *CognitiveComplexityRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
 | |
| 	var failures []lint.Failure
 | |
| 
 | |
| 	const expectedArgumentsCount = 1
 | |
| 	if len(arguments) < expectedArgumentsCount {
 | |
| 		panic(fmt.Sprintf("not enough arguments for cognitive-complexity, expected %d, got %d", expectedArgumentsCount, len(arguments)))
 | |
| 	}
 | |
| 	complexity, ok := arguments[0].(int64)
 | |
| 	if !ok {
 | |
| 		panic(fmt.Sprintf("invalid argument type for cognitive-complexity, expected int64, got %T", arguments[0]))
 | |
| 	}
 | |
| 
 | |
| 	linter := cognitiveComplexityLinter{
 | |
| 		file:          file,
 | |
| 		maxComplexity: int(complexity),
 | |
| 		onFailure: func(failure lint.Failure) {
 | |
| 			failures = append(failures, failure)
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	linter.lint()
 | |
| 
 | |
| 	return failures
 | |
| }
 | |
| 
 | |
| // Name returns the rule name.
 | |
| func (r *CognitiveComplexityRule) Name() string {
 | |
| 	return "cognitive-complexity"
 | |
| }
 | |
| 
 | |
| type cognitiveComplexityLinter struct {
 | |
| 	file          *lint.File
 | |
| 	maxComplexity int
 | |
| 	onFailure     func(lint.Failure)
 | |
| }
 | |
| 
 | |
| func (w cognitiveComplexityLinter) lint() {
 | |
| 	f := w.file
 | |
| 	for _, decl := range f.AST.Decls {
 | |
| 		if fn, ok := decl.(*ast.FuncDecl); ok {
 | |
| 			v := cognitiveComplexityVisitor{}
 | |
| 			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 {
 | |
| 	complexity   int
 | |
| 	nestingLevel int
 | |
| }
 | |
| 
 | |
| // subTreeComplexity calculates the cognitive complexity of an AST-subtree.
 | |
| 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 += 1
 | |
| 		}
 | |
| 	}
 | |
| 	// TODO handle (at least) direct recursion
 | |
| 
 | |
| 	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)
 | |
| 
 | |
| 	return calculator.complexity
 | |
| }
 | |
| 
 | |
| type binExprComplexityCalculator struct {
 | |
| 	complexity    int
 | |
| 	opsStack      []token.Token // stack of bool operators
 | |
| 	subexpStarted bool
 | |
| }
 | |
| 
 | |
| 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
 | |
| 		}
 | |
| 
 | |
| 		becc.opsStack = append(becc.opsStack, n.Op)
 | |
| 	case *ast.ParenExpr:
 | |
| 		becc.subexpStarted = true
 | |
| 	}
 | |
| 
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| 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
 | |
| }
 |