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 }