jfmengels / elm-review-cognitive-complexity / CognitiveComplexity

rule : Basics.Int -> Review.Rule.Rule

Reports functions that have a too high cognitive complexity.

You can configure the threshold above which a function will be reported (15 in the example configuration below).

config =
    [ CognitiveComplexity.rule 15
    ]

What is cognitive complexity?

Cognitive complexity is not to be confused with "Cyclomatic Complexity", which has a different way of measuring the complexity.

Here's an explanation extracted from free white paper provided by SonarSource, the creators of the concept.

Cognitive complexity tries to measure how hard it is to understand a function, primarily focusing on the control structures that hinder the understanding of a function by reading it from top to bottom in one go, like you would for a novel.

A Cognitive Complexity score is assessed according to three basic rules:

  1. Ignore structures that allow multiple statements to be readably shorthanded into one
  2. Increment (add one) for each break in the linear flow of the code
  3. Increment when flow-breaking structures are nested

Some small differences may be found between the implementation detailed in the paper and this rule, as the idea was formulated more on imperative programming languages, and may not be applicable to a pure functional language like Elm.

You can read about how is works in the complexity breakdown section below.

When (not) to enable this rule

This rule is an experiment. I don't know if this will be more useful or detrimental, and I haven't yet figured out what the ideal complexity threshold for Elm projects is.

I would for now recommend to use it with a very high threshold to find places in your codebase that need refactoring, and eventually to enable it in your configuration to make sure no new extremely complex functions appear. As you refactor more and more of your codebase, you can gradually lower the threshold until you reach a level that you feel happy with.

Please let me know how enabling this rule works out for you! If enforcing doesn't work for you, then you can use this as an insight rule instead.

Use as an insight rule

If instead of enforcing a threshold, you wish to have an overview of the complexity for each function, you can run the rule as an insight rule (using elm-review --report=json --extract), which would yield an output like the following:

{
  "Some.Module": {
    "someFunction": 16,
    "someOtherFunction": 0
  },
  "Some.Other.Module": {
    "awesomeFunction": 2
  }
}

Complexity breakdown

Following is a breakdown of how the complexity of a function is computed:

-- Total: 4
a =
  if b then           -- +1
    if c then         -- +2, including 1 for nesting
      1
    else
      2
  else if d then      -- +1
      3
  else                -- +0
      4
-- Total: 3
a =
  case b of -- +1
    A -> 1
    B -> 2
    C ->
      case c of -- +2, including 1 for nesting
        _ -> 3
    D -> 4
-- Total: 2
a =
  let
    fn b =   -- increases nesting
      if b then    -- +2, including 1 for nesting
        1
      else
        2

    constant = -- Not a function, no increase
      True
  in
  fn constant
-- Total: 2
a things =
  List.map
    (\thing ->        -- increases nesting
      case thing of   -- +2, including 1 for nesting
        Just _ -> 1
        Nothing -> 2
    )
    things
a && b -- +1

-- This is still the same logical construction as
-- above, and therefore about as hard to understand
a && b && c && d && e -- +1

-- Total: 3
a && b && c -- +1
  || d -- +1 for breaking the chain of && with a ||
  || not (e || f) -- +1 for breaking the chain
                  -- with a `not` of a binary operation
-- Total: 2
fun1 n =
  fun2 n    -- +1
  + fun2 n  -- +0, already counted
  + fun1 n  -- +1

-- Total: 1
fun2 n =
  fun1 n    -- +1

The original metric increases the complexity for other structures that the Elm language doesn't have.

Try it out

You can try this rule out by running the following command:

elm-review --template jfmengels/elm-review-cognitive-complexity/example --rules CognitiveComplexity

The cognitive complexity is set to 15 in the configuration used by the example.

If instead of enforcing a threshold, you wish to have an overview of the complexity for each function, you can run the rule like this (requires jq):

elm-review --template jfmengels/elm-review-cognitive-complexity/example --extract --report=json --rules CognitiveComplexity | jq -r '.extracts.CognitiveComplexity'

Thanks

Thanks to the team at SonarSource for designing the metric and for not restricting its use. Thanks to G. Ann Campbell for the different talks she made on the subject.