自分のプロジェクトで Code Climate を使ってみた時の話をします。
Code Climate とは?
- コードの品質とかを測れるサービス
- OSS なら無料で利用可能
使ってみた結果
私のコードに対して、下記のような指摘が来ました。
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 |
機械的なテストの難しさを測る | 人間の理解の難しさを測る |
Cyclomatic Complexity と Cognitive Complexity の違いの例
たとえば下記2つのコードの、どちらが"複雑"だと思いますか?
// 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;
}
}
// 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"
なにもないメソッドは、複雑度ゼロです。
// Cognitive Complexity: 0
function method1() {
}
そこに if
文や 例外の catch
のように、"コードの線形的な流れを乱す要素"が加わると、+1 されます
// Cognitive Complexity: 1
function method2() {
if (true) { // +1
}
}
// Cognitive Complexity: 1
function method3() {
try {
} catch (e) { // +1
}
}
組み合わせると、Complexity は増えていきます。
// 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
などがネストされていると、ネストされたぶんだけ複雑度が加算されていきます。
たとえば下記のコード。
function method1() {
if (true) {
if (true) {
}
}
}
ひとつめの if
は複雑度 +1 です。
しかしふたつめの if
は、ひとつネストされた分が加算されて、複雑度 +2 となります。
つまり、合計の Cognitive Complexity は 1 + 2 = 3 になります。
// Cognitive Complexity: 3
function method1() {
if (true) { // +1
if (true) { // +2 (= if +1, nest +1)
}
}
}
そのため下記ふたつのメソッドを比べたときに、前者の「ネストされた if
」の方が、後者の「ネストされていない if
」よりも複雑であると、定量的に判断されます。
// 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 になります。
# Cognitive Complexity: 2
def method1()
if obj && obj.func?
return true
end
false
end
ここで、Ruby の safe navigation(ぼっち演算子) &.
3 を使うと、論理演算子 &&
を減らすことができます。
このとき複雑度は 1 となります。
# Cognitive Complexity: 1
def method2()
if obj&.obj.func?
return true
end
false
end
ちなみに、当然下記のように if
も無くしてしまえば複雑度はゼロです。
# Cognitive Complexity: 0
def method3()
obj&.obj.func?
end
実際の指摘
冒頭に挙げた Code Climate で Cognitive Complexity を 8 から 5 にしろと指摘された例を見てみます。
// 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 の使い方
ローカルで計測できるやつ作った
index.ts
や index.rb
を適当に編集して $ docker
コマンドを打つと、Cognitive Complexity を算出します。Windows は非対応なので Linux 環境で使ってください。
$ 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 の Docker イメージを持ってきて Cognitive Complexity だけ 許容値-1(ゼロ以上でアラート)で動かしているだけです。
まとめ
- ネストを減らして、分岐を減らして、複雑度の低いコードを書こう
- Cognitive Complexity を CI で測りたい?
- Code Climate を使おう
注意事項: NGな使い方
- 「お前のコード Cognitive Complexity 高すぎない?」と新人にコードレビューしてはいけません
- 新人が死にます 4
- 「俺なら45分で直せる」と新人にコードレビューしてはいけません
- 新人が死にます 4