github.com/Johnny2210/revive@v1.0.8-0.20210625134200-febf37ccd0f5/rule/cognitive-complexity.go (about)

     1  package rule
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  
     8  	"github.com/mgechev/revive/lint"
     9  	"golang.org/x/tools/go/ast/astutil"
    10  )
    11  
    12  // CognitiveComplexityRule lints given else constructs.
    13  type CognitiveComplexityRule struct{}
    14  
    15  // Apply applies the rule to given file.
    16  func (r *CognitiveComplexityRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
    17  	var failures []lint.Failure
    18  
    19  	const expectedArgumentsCount = 1
    20  	if len(arguments) < expectedArgumentsCount {
    21  		panic(fmt.Sprintf("not enough arguments for cognitive-complexity, expected %d, got %d", expectedArgumentsCount, len(arguments)))
    22  	}
    23  	complexity, ok := arguments[0].(int64)
    24  	if !ok {
    25  		panic(fmt.Sprintf("invalid argument type for cognitive-complexity, expected int64, got %T", arguments[0]))
    26  	}
    27  
    28  	linter := cognitiveComplexityLinter{
    29  		file:          file,
    30  		maxComplexity: int(complexity),
    31  		onFailure: func(failure lint.Failure) {
    32  			failures = append(failures, failure)
    33  		},
    34  	}
    35  
    36  	linter.lint()
    37  
    38  	return failures
    39  }
    40  
    41  // Name returns the rule name.
    42  func (r *CognitiveComplexityRule) Name() string {
    43  	return "cognitive-complexity"
    44  }
    45  
    46  type cognitiveComplexityLinter struct {
    47  	file          *lint.File
    48  	maxComplexity int
    49  	onFailure     func(lint.Failure)
    50  }
    51  
    52  func (w cognitiveComplexityLinter) lint() {
    53  	f := w.file
    54  	for _, decl := range f.AST.Decls {
    55  		if fn, ok := decl.(*ast.FuncDecl); ok && fn.Body != nil {
    56  			v := cognitiveComplexityVisitor{}
    57  			c := v.subTreeComplexity(fn.Body)
    58  			if c > w.maxComplexity {
    59  				w.onFailure(lint.Failure{
    60  					Confidence: 1,
    61  					Category:   "maintenance",
    62  					Failure:    fmt.Sprintf("function %s has cognitive complexity %d (> max enabled %d)", funcName(fn), c, w.maxComplexity),
    63  					Node:       fn,
    64  				})
    65  			}
    66  		}
    67  	}
    68  }
    69  
    70  type cognitiveComplexityVisitor struct {
    71  	complexity   int
    72  	nestingLevel int
    73  }
    74  
    75  // subTreeComplexity calculates the cognitive complexity of an AST-subtree.
    76  func (v cognitiveComplexityVisitor) subTreeComplexity(n ast.Node) int {
    77  	ast.Walk(&v, n)
    78  	return v.complexity
    79  }
    80  
    81  // Visit implements the ast.Visitor interface.
    82  func (v *cognitiveComplexityVisitor) Visit(n ast.Node) ast.Visitor {
    83  	switch n := n.(type) {
    84  	case *ast.IfStmt:
    85  		targets := []ast.Node{n.Cond, n.Body, n.Else}
    86  		v.walk(1, targets...)
    87  		return nil
    88  	case *ast.ForStmt:
    89  		targets := []ast.Node{n.Cond, n.Body}
    90  		v.walk(1, targets...)
    91  		return nil
    92  	case *ast.RangeStmt:
    93  		v.walk(1, n.Body)
    94  		return nil
    95  	case *ast.SelectStmt:
    96  		v.walk(1, n.Body)
    97  		return nil
    98  	case *ast.SwitchStmt:
    99  		v.walk(1, n.Body)
   100  		return nil
   101  	case *ast.TypeSwitchStmt:
   102  		v.walk(1, n.Body)
   103  		return nil
   104  	case *ast.FuncLit:
   105  		v.walk(0, n.Body) // do not increment the complexity, just do the nesting
   106  		return nil
   107  	case *ast.BinaryExpr:
   108  		v.complexity += v.binExpComplexity(n)
   109  		return nil // skip visiting binexp sub-tree (already visited by binExpComplexity)
   110  	case *ast.BranchStmt:
   111  		if n.Label != nil {
   112  			v.complexity++
   113  		}
   114  	}
   115  	// TODO handle (at least) direct recursion
   116  
   117  	return v
   118  }
   119  
   120  func (v *cognitiveComplexityVisitor) walk(complexityIncrement int, targets ...ast.Node) {
   121  	v.complexity += complexityIncrement + v.nestingLevel
   122  	nesting := v.nestingLevel
   123  	v.nestingLevel++
   124  
   125  	for _, t := range targets {
   126  		if t == nil {
   127  			continue
   128  		}
   129  
   130  		ast.Walk(v, t)
   131  	}
   132  
   133  	v.nestingLevel = nesting
   134  }
   135  
   136  func (cognitiveComplexityVisitor) binExpComplexity(n *ast.BinaryExpr) int {
   137  	calculator := binExprComplexityCalculator{opsStack: []token.Token{}}
   138  
   139  	astutil.Apply(n, calculator.pre, calculator.post)
   140  
   141  	return calculator.complexity
   142  }
   143  
   144  type binExprComplexityCalculator struct {
   145  	complexity    int
   146  	opsStack      []token.Token // stack of bool operators
   147  	subexpStarted bool
   148  }
   149  
   150  func (becc *binExprComplexityCalculator) pre(c *astutil.Cursor) bool {
   151  	switch n := c.Node().(type) {
   152  	case *ast.BinaryExpr:
   153  		isBoolOp := n.Op == token.LAND || n.Op == token.LOR
   154  		if !isBoolOp {
   155  			break
   156  		}
   157  
   158  		ops := len(becc.opsStack)
   159  		// if
   160  		// 		is the first boolop in the expression OR
   161  		// 		is the first boolop inside a subexpression (...) OR
   162  		//		is not the same to the previous one
   163  		// then
   164  		//      increment complexity
   165  		if ops == 0 || becc.subexpStarted || n.Op != becc.opsStack[ops-1] {
   166  			becc.complexity++
   167  			becc.subexpStarted = false
   168  		}
   169  
   170  		becc.opsStack = append(becc.opsStack, n.Op)
   171  	case *ast.ParenExpr:
   172  		becc.subexpStarted = true
   173  	}
   174  
   175  	return true
   176  }
   177  
   178  func (becc *binExprComplexityCalculator) post(c *astutil.Cursor) bool {
   179  	switch n := c.Node().(type) {
   180  	case *ast.BinaryExpr:
   181  		isBoolOp := n.Op == token.LAND || n.Op == token.LOR
   182  		if !isBoolOp {
   183  			break
   184  		}
   185  
   186  		ops := len(becc.opsStack)
   187  		if ops > 0 {
   188  			becc.opsStack = becc.opsStack[:ops-1]
   189  		}
   190  	case *ast.ParenExpr:
   191  		becc.subexpStarted = false
   192  	}
   193  
   194  	return true
   195  }