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 }