github.com/songshiyun/revive@v1.1.5-0.20220323112655-f8433a19b3c5/rule/cognitive-complexity.go (about)

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