You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cognitive-complexity.go 4.6KB

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