Cognitive Complexity で、コードの読みやすさを定量的に計測しよう

自分のプロジェクトで [Code Climate](https://codeclimate.com/) を使ってみた時の話をします。

## [Code Climate](https://codeclimate.com/) とは？
- コードの品質とかを測れるサービス
- OSS なら無料で利用可能

### 使ってみた結果
[私のコード](https://codeclimate.com/github/s2terminal/i-read-u/issues#)に対して、下記のような指摘が来ました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/50391/300f5f18-e182-7003-61c9-ecf7ae0d531c.png)

>*Function `toCommandSections` has 29 lines of code (exceeds 25 allowed).*
(`toCommandSections`メソッドが29行あるから、25行以下にしてくれ)
>*About 1 hr to fix*
(1時間あれば直せる)

こちらは分かりやすい指摘です。
ただ"1時間で直せる"とは誰がどういう見解で言っているのか納得行きません。

>*Function `read` has a* **Cognitive Complexity** *of 8 (exceeds 5 allowed).*
(`read`メソッドの **Cognitive Complexity** が8だから、5以下にしてくれ)
>*About 45 mins to fix*
(45分で直せる)

**Cognitive Complexity...？**一体なにを指摘されているのか分かりません。
しかも About 45 mins to fix と来ました。一体誰が？

## Cognitive Complexity とは何か？
「コグニティブ・コンプレクシティー」、訳してみれば「認知的複雑度」でしょうか。
コードの複雑さを測る指標のひとつのようです。

ググると心理的の記事が多くヒットしますが、同名の用語と関係は無いようです。

### 「Cyclomatic Complexity」と「Cognitive Complexity」
Cognitive Complexity と似たものに**「Cyclomatic Complexity(サイクロマティック・コンプレクシティー、循環的複雑度)」**があります。こちらの方が一般によく知られていると思います。

Cyclomatic Complexity は以前から提唱されていましたが、その問題点を改善するための指標として Cognitive Complexity が考え出された、とのこと。

違いをまとめると、下記のような感じです。

Cyclomatic Complexity|Cognitive Complexity
:--:|:--:
サイクロマティック・コンプレクシティー|コグニティブ・コンプレクシティー
循環的複雑度|認知的複雑度
1976年 Thomas J. McCabe 氏が考案[^1]|2016年 SonarSource 社が考案[^2]
機械的なテストの難しさを測る|人間の理解の難しさを測る

[^1]: [A Complexity Measure - Thomas J. McCabe](http://www.literateprogramming.com/mccabe.pdf)
[^2]: [COGNITIVE COMPLEXITY \- A new way of measuring understandability \- G. Ann Campbell](https://www.sonarsource.com/docs/CognitiveComplexity.pdf)

### Cyclomatic Complexity と Cognitive Complexity の違いの例
たとえば下記2つのコードの、どちらが"複雑"だと思いますか？

```typescript:typescript
// method1: switch文
function method1(n: number) {
  switch (n) {
    case 1:
      console.log("ichi");
      break;
    case 2:
      console.log("ni");
      break;
    case 3:
      console.log("san");
      break;
    default:
      console.log("ippai");
      break;
  }
}
```

```typescript:typescript
// method2: ふたつの for 文のネスト
function method2(max: number) {
  let total = 0;
  labelA:
  for (let i = 1; i <= max; i++) {
    for (let j = 2; j < i; j++) {
      if (i % j == 0) {
        continue labelA;
      }
    }
    total += 1;
  }

  return total;
}
```

「どちらが複雑か？」と言われれば、「前者の switch 文よりも、 後者の for と if のネストのほうが複雑」のように見えます。

しかし、Cyclomatic Complexity と Cognitive Complexity をそれぞれ算出すると、下記のようになります。

method|内容|Cyclomatic Complexity|Cognitive Complexity
:--|:--|:--|:--
method1|switch文ひとつ|**4**|**1**
method2|forとifのネスト|**4**|**7**

Cyclomatic Complexity は両方とも 4 で同値、つまり**ふたつのメソッドの複雑度は同じ**、となりました。
一方 Cognitive Complexity は、前者は 1 、後者は 7 と明らかな差が付きました。 **switch 文のほうが単純であるという事が、定量的に示されました**。

switch 文ひとつであっても分岐網羅テストケースは 4 パターン必要になるため、 Cyclomatic complexity は 4 になります。しかしヒトが理解するには Switch 文のほうが明らかに単純ですので、 Cognitive Complexity の方は小さい値をとります。

## Cognitive Complexity の実際の測り方
Cognitive Complexity は、基本的に下記3つのシンプルな法則に伴って算出されます。

### コードの線形的な流れを乱すとき、複雑とみなす
>*Code is considered more complex for each "break in the linear flow of the code"*

なにもないメソッドは、複雑度ゼロです。

```typescript:typescript
// Cognitive Complexity: 0
function method1() {
}
```
そこに `if` 文や 例外の `catch` のように、"コードの線形的な流れを乱す要素"が加わると、+1 されます

```typescript:typescript
// Cognitive Complexity: 1
function method2() {
  if (true) { // +1
  }
}
```

```typescript:typescript
// Cognitive Complexity: 1
function method3() {
  try {
  } catch (e) { // +1
  }
}
```
組み合わせると、Complexity は増えていきます。

```typescript:typescript
// Cognitive Complexity: 2
function method4() {
  try {
    if (true) { // +1
    }
  } catch (e) { // +1
  }
}

// Cognitive Complexity: 2
function method5() {
  if (true) { // +1
  } else {    // +1
  }
}
```
他にもループや `switch` 文、論理演算子やラベルジャンプ等が加わると、複雑度が加算されていきます。

### 流れを乱すネストが深いほど、複雑とみなす
>Code is considered more complex when "flow breaking structures are nested"

ここからが面白いところです。
`if` などがネストされていると、ネストされたぶんだけ複雑度が加算されていきます。

たとえば下記のコード。

```typescript:typescript
function method1() {
  if (true) {
    if (true) {
    }
  }
}
```
ひとつめの `if` は複雑度 +1 です。
しかしふたつめの `if` は、ひとつネストされた分が加算されて、複雑度 +2 となります。

つまり、合計の Cognitive Complexity は **1 + 2 = 3** になります。

```typescript:typescript
// Cognitive Complexity: 3
function method1() {
  if (true) {   // +1
    if (true) { // +2 (= if +1, nest +1)
    }
  }
}
```

そのため下記ふたつのメソッドを比べたときに、前者の「ネストされた `if` 」の方が、後者の「ネストされていない `if` 」よりも複雑であると、定量的に判断されます。

```typescript:typescript
// Cognitive Complexity: 3 こちらのほうが複雑
function method1() {
  if (true) {   // +1
    if (true) { // +2 (= if +1, nest +1)
    }
  }
}

// Cognitive Complexity: 2 こちらのほうが単純
function method2() {
  if (true) { // +1
  }
  if (true) { // +1
  }
}
```

### ショートハンドを使って複数の文をひとつにまとめていれば、複雑さが高いとはみなされない
>Code is not considered more complex when it uses shorthand that the language provides for collapsing multiple statements into one

たとえば下記のRubyコードは、 `if` 文と論理演算子 `&&` がそれぞれ +1 加算され、複雑度は 2 になります。

```ruby:ruby
# Cognitive Complexity: 2
def method1()
  if obj && obj.func?
    return true
  end

  false
end
```

ここで、Ruby の safe navigation(ぼっち演算子) `&.` [^3] を使うと、論理演算子 `&&` を減らすことができます。
このとき複雑度は 1 となります。

[^3]: [NEWS for Ruby 2\.3\.0 \(Ruby 2\.6\.0\)](https://docs.ruby-lang.org/ja/latest/doc/news=2f2_3_0.html)

```ruby:ruby
# Cognitive Complexity: 1
def method2()
  if obj&.obj.func?
    return true
  end

  false
end
```

ちなみに、当然下記のように `if` も無くしてしまえば複雑度はゼロです。

```ruby:ruby
# Cognitive Complexity: 0
def method3()
  obj&.obj.func?
end
```

### 実際の指摘
冒頭に挙げた [Code Climate で Cognitive Complexity を 8 から 5 にしろと指摘された例](https://codeclimate.com/github/s2terminal/i-read-u/src/index.ts/source#issue-08e7c6740570d84df18ccc85fc546b9b)を見てみます。

```typescript:typescript
// Cognitive Complexity: 8
function read(filepath: string, __: (key: string) => string): string {
  try {
    return fs.readFileSync(format(parse(filepath)), "utf8");
  } catch (e) {                                 // +1
    if (e instanceof Error) {                   // +2  (= if +1, nest +1)
      if (e.message.indexOf("ENOENT") === 0) {  // +3  (= if +1, nest +2)
        // tslint:disable-next-line:no-console
        console.log(__("FileNotFound"));
      } else {                                  // +1
        throw e;
      }
    } else {                                    // +1
      throw e;
    }
  }
 
  return "";
}
```
`try` ～ `catch` と `if` で3重のネストになって複雑度が増えていた事がわかります。確かに読みづらいコードになっています。

## Cognitive Complexity の使い方

### ローカルで計測できるやつ作った
https://github.com/s2terminal/cognitive-complexity-example

`index.ts` や `index.rb` を適当に編集して `$ docker` コマンドを打つと、Cognitive Complexity を算出します。Windows は非対応なので Linux 環境で使ってください。

```bash
$ docker-compose run --rm analyze
Starting analysis
Running structure: Done!

== index.js (3 issues) ==
2-3: Function `method0` has a Cognitive Complexity of 0 (exceeds -1 allowed). Consider refactoring. [structure]
7-12: Function `method1` has a Cognitive Complexity of 2 (exceeds -1 allowed). Consider refactoring. [structure]
16-21: Function `method2` has a Cognitive Complexity of 3 (exceeds -1 allowed). Consider refactoring. [structure]
```

やっていることは、[CodeClimate CLI](https://github.com/codeclimate/codeclimate) の [Docker イメージ](https://hub.docker.com/r/codeclimate/codeclimate)を持ってきて Cognitive Complexity だけ 許容値-1(ゼロ以上でアラート)で動かしているだけです。

### まとめ
- ネストを減らして、分岐を減らして、複雑度の低いコードを書こう
- Cognitive Complexity を CI で測りたい？
    - [Code Climate](https://codeclimate.com/) を使おう

### 注意事項: NGな使い方
- 「お前のコード Cognitive Complexity 高すぎない？」と新人にコードレビューしてはいけません
    - 新人が死にます [^4]
- 「俺なら45分で直せる」と新人にコードレビューしてはいけません
    - 新人が死にます [^4]

[^4]: [新人プログラマをレビューで殺さない方法 \- Qiita](https://qiita.com/hiraike32/items/32840b11536fa1b78621#%E3%82%8F%E3%81%8B%E3%82%89%E3%81%AA%E3%81%84%E5%8D%98%E8%AA%9E%E3%82%92%E4%B8%A6%E3%81%B9%E3%82%8B)


## 参考文献
- [Cognitive Complexity \- Code Climate](https://docs.codeclimate.com/docs/cognitive-complexity)
- [COGNITIVE COMPLEXITY \- A new way of measuring understandability \- G. Ann Campbell](https://www.sonarsource.com/docs/CognitiveComplexity.pdf)