LoginSignup
43
22

More than 3 years have passed since last update.

DEAD CODE COOKBOOK ~デッドコードの作り方と復活の呪文~ の紹介

Last updated at Posted at 2020-11-30

この記事は 闇の魔術に対する防衛術 Advent Calendar 2020 の1日目の記事です。

2日目も埋まっていなかったので 存在しない正規表現 という記事を書きます。

参加しているカレンダーとして完走させたいので、是非参加してください!
枠はまだまだ空いています!


DEAD CODE COOKBOOK ~デッドコードの作り方と復活の呪文~ を11/30に公開しました。

DEAD CODE COOKBOOK_resize.png

以下は、「はじめに」「🔖中断を利用するパターン」「🧪return後のコード」「👼ネスト修正による中断コードの移動」「🧟goto文のラベルによるジャンプ」「🧟ホイスティング」を一部抜粋した紹介となります。


本書の目的

デッドコードは、 プログラムの一部として存在するが、決して実行されないコード のことである。一時的な変更等でデッドコードを作る場合もあるが、多くの場合は バグ(プログラム自体の誤り)の可能性が極めて高い だろう。例えば 「goto fail bug1」 がデッドコードを含んだ有名なバグの実例だ。

そのため、理想的にはプログラミング言語レベルでデッドコードを検知し、警告出力や実行不可などの対応をとることが望ましい。実際に、特定のパターンのデッドコードは言語仕様上許されていない場合もある。

しかし、任意のコードが到達不能(デッドコード)かどうかを判断することは停止性問題を解くことと等価であり、 あらゆるデッドコードを正しく把握することは不可能2 である。デッドコードを完全に排除している汎用プログラミング言語は、今のところ存在していないだろう。

では、実際のプログラミングのコードには、どのようなデッドコードのパターンがあるのだろうか?

この疑問の答えを求めるのが本書である。

しかしながら、デッドコードは除去すべきものであり、デザインパターンのように、再利用を促すものではない。では、 デッドコードのパターンから何が得られるのだろうか?

デッドコードのパターンを整理するためには、プログラミングのパターンを整理する必要がある。それは、単なるデッドコードを回避するという直接的な理由以外に、 プログラミングのパターンやスタイルについての考察を深める ことにも繋がる。

また、デッドコードが実行可能かどうかは、プログラミング言語によっても異なる。また、あるプログラミング言語ではデッドコードとなるパターンであっても、高度な機能や特殊な記載を用いることで、別のプログラミング言語ではコードを通すことが出来る場合もある。そのため、デッドコードのパターンを整理し、プログラミング言語毎の具体例を確認することで、 各プログラミング言語への理解を深める ことにも繋がる。

あるいは、実際のプロジェクトで、前任者が書いたデッドコードに出くわした経験がある人も多いだろう。デッドコードの除去はソースの可読性を向上させ、保守のしやすさにつながる。しかし、デッドコードの除去で動作が変わらない(影響がない)と言えるだろうか?リスク回避という観点からデッドコードの除去を断念することもあるだろう。もし、該当のデッドコードに関する考察がすでになされていて、注意点がまとまっていればどうだろうか? 正確なデッドコードの除去を後押しする ことができるだろう。

これらが、本書の目的である。

つまり、 デッドコードのパターンを通して 、プログラミングのパターンやスタイルについての考察を深め、各プログラム言語への理解を深め、正確なデッドコードの除去を後押しし、 より良いコードを書く ことである。

本書のタイトルは「DEAD CODE COOKBOOK」、デッドコードのレシピ集である。

しかし、本書は 意図的にデッドコードを作るためのレシピ集ではない。

見つけ難い不具合を意図的に埋め込むためではなく、 より良いコードを書く ためにのみ活用してほしい。

対象範囲と用語の定義

☠デッドコード☠

デッドコード(dead coad) は一般的に到達不能なコードを表わす言葉である。類語の 到達不能コード(unreachable code) との使い分けは行わず、本書ではデッドコードで統一する。

本書のデッドコードは、以下の2点を対象外として扱う。

  • 実行時のコード(機械語、あるいは元のプログラムよりも低い水準のコード)
  • 実行結果から、処理の有無を確認できないコード

上記により、 大半のオプティマイザで削除されるであろう、未参照変数に対する代入や、未使用のシグネチャ定義は対象外 とする。なお、未使用の関数定義や、余分なコールバック引数は、処理されていないことが確認できるため、対象とする。

また、本書のデッドコードは 厳密に到達不能のコードのみではなく、後述するデッドコードレシピに基づいたコードも含む

🧪デッドコードレシピ🧪

デッドコードの作り方、つまり、デッドコードを実現できる プログラミング言語の機能の組み合わせを、「デッドコードレシピ」 と定義する。

デッドコードレシピに従ったパターンによるソースコードであれば、デッドコードとする。つまり、厳密には到達可能であっても、 分別のあるプログラミング3では到達不能と認識されるソースコードはデッドコード とする。たとえば、プロトタイプ汚染のみにより到達可能なコードはデッドコードである。

— 序文 - まつもとゆきひろ “メタプログラミングRuby 第2版”, Paolo Perrotta.

本書では、デッドコードレシピについて、 複数のプログラミング言語でサンプルコードを記載 する。

🔖デッドコードパターン🔖

デッドコードレシピの分類を「デッドコードパターン」 とする。本書では、デッドコードパターン毎にデッドコードレシピを記載する。

🔪デッドコードツール🔪

デッドコードを作るうえで、 関連深いトピックや、共通して使えるプログラミングの部品を「デッドコードツール」 とする。デッドコードパターンがデッドコードレシピに対するグループ化であるのに対し、デッドコードツールはデッドコードレシピに対する詳細化である。

🛐供養🛐

デッドコードの修正時に 該当部分のみを削除することを「供養」 と定義する。

通常、デッドコードは存在しないほうが望ましい。デッドコードを削除するにあたり、挙動を変えない場合は該当部分のみを削除、つまり供養すればよい。

ただし、 供養してはいけないケースがある 点には注意が必要だ。供養する場合は、次の2点を気にする必要が有るだろう。

1つめは、そのデッドコードに関わる処理に、 バグが存在しないかの確認 だ。そもそも、バグによりデッドコードになっている場合に供養すると、挙動は変わらないが、バグが残る。バグを残さないためには、後述のレイズによりデッドコードが実行されるように修正する必要がある。

2つめは、一見到達不能に見えるが、 厳密には到達可能ではないかの確認 だ。一見到達不能に見えるが、厳密には到達可能なデッドコードの場合は、供養により挙動が変わってしまう。後述のゾンビ化によりソースコードが実行されていないかを確認する必要がある。

このことから、供養については 方法は簡単だが、判断が難しい といえる。供養には、時として勇気が必要となる。

本書では、 供養の方法は明確(該当部分の削除)なため逐一記載しない 。しかし、後述する復活の呪文はなるべく詳細に記載する。それは、復活の呪文の知識が、供養の判断の後押しをするためである。

供養を躊躇ってもよいが、怠ってはいけない。大半のデッドコードは供養するのが正解である。

🧙復活の呪文🧙

デッドコードの処理を 実行するための方法を「復活の呪文」 と定義する。

復活の呪文は、2種類に分けることができる。

1つめは、 該当箇所のソースコードの書き換えを伴う もので、これを「レイズ」として後述する。

2つめは、 該当箇所のソースコードの書き換えを伴わない もので、これを「ゾンビ化」として後述する。

なお、復活させずにソースコードから削除する方法は、前述の供養である。供養は復活の呪文ではない。

👼レイズ👼

ソースコードを書き換えて、デッドコードを到達可能にすることを「レイズ」 と定義する。

レイズは、デッドコードを作り込んだ理由を想定し、ソースコードを正しい状態にする。プログラムの挙動が変わる ため、レイズの正しさは、個別のコード毎に判断して慎重に実施する必要がある。

レイズされたデッドコードは、 デッドコードレシピから外れるため、デッドコードではなくなる

レイズが必要なデッドコードは、レイズすべき である。しかし、意図して到達不能にしたデッドコードに対しては、レイズを行ってはいけない。

本書は、誤って供養しないためにレイズについての記載を行う。しかし、 多くの場合は供養が正解であり、レイズが正解ではない点 には注意してほしい。

🧟ゾンビ化🧟

該当箇所のソースコードを書き換えずに、デッドコードを実行にすることを「ゾンビ化」 と定義する。

厳密には到達可能なデッドコードについては、分別のあるプログラミングでは実行することが出来ない。つまり、高度な機能や特殊な記載を用いる必要がある。本書では、 各プログラミング言語の理解を深めること と、 ゾンビ化によってデッドコードが有効になっていないことの確認 のために、ゾンビ化の記載を行う。

ゾンビ化によって有効になっているデッドコードは特殊であり、対処も難しい だろう。本書では、これらに対する対処は対象外として記載しない。

ほとんどの場合、 ゾンビ化によって有効になっているデッドコードは可読性を落とし、保守性を著しく低下させる点 には注意してほしい。

🐕ケルベロス🐕

前提条件担保のため、意図的に設けられたデッドコードを「ケルベロス」 と定義する。一般的には、assertion(表明)と呼ばれる概念4 が存在するが、より範囲を限定した定義としてケルベロスを用いる。

デッドコードは、基本的に可読性の観点から望ましくない。到達不能なデッドコードに、あたかも意味があるような処理が記載されていた場合、コードの解読に必要以上に(もっといえば、無意味に)時間がかかってしまうだろう。しかし、 到達不能なデッドコードに、到達不能を想定している記載がある場合 はどうだろうか?ガード節(Guard Clause) のように 可読性を向上させる場合もある だろう。(ケルベロスは冥界の番犬 ≒ デッドコードの番人である)

実際に、 「念のため」という言葉が付いたデッドコードを目にした経験 がある人も多いのではないだろうか。

もちろん、厳密に到達不能なデッドコードは、カバレッジ等で扱いが複雑になるため、ケルベロスを推奨するわけではない。しかし、 たとえばTypeScriptのnever型5 などの機能を考慮すると、プログラミングスタイルやテストツール環境によっては、 到達不能なデッドコードに、到達不能を想定している記載 をして可読性をあげる選択肢もある。

本書では、 ケルベロスの是非は議論しない が、デッドコードレシピが ケルベロスとして適用できるかは記載 する。

本書のデッドコードに対する考え方

  • ☠️デッドコード☠️ は原則として邪悪(evil)であり、取り除くべきである。
  • プログラミングスタイルによっては、🐕ケルベロス🐕 は許容される場合がある。
  • 単純に 🛐供養🛐 するのではなく、🧙復活の呪文🧙 を考慮する。
    • 👼レイズ👼 が必要か?
    • 🧟ゾンビ化🧟 で有効になってないか?
  • 🧪デッドコードレシピ🧪 をアンチパターンとすることで、より良いコードを書くことができる。

対象読者

本書は、 プログラミング経験者を対象とし、プログラミングの基礎についての説明は行わない 。しかし、本書は特定のプログラミング言語に限定しないため、 初心者でも分かる説明 で記載する。デッドコード自体は中級者以降では作りこむことはほとんどない だろう。しかし、中級者でも 他のプログラミング言語に関する知識や、ゾンビ化に用いる高度な機能は参考になる だろう。そのため、 本書は習熟度によって違った楽しみ方 ができるだろう。

初心者: プログラミングにおける機能の確認。デッドコードという アンチパターンを通した、プログラミングスタイルの学習

中級者以降: 各プログラミング言語毎の高度な機能の確認 。プログラミング言語毎の、各種機能のサポート状況等の確認。

🔖中断を利用するパターン

構造化プログラミングは、原則として、順次、つまり上から下に実行される。

🔖中断を利用するパターン は、中断、すなわち、それ以降処理が実行されない記載を利用する。 中断された処理以降に記載されているコードは、デッドコードとなる

中断させるためには、 return などの予約語 を用いることができる。予約語のみではなく、 組み込み関数の exit や、 中断させる関数を定義して使用 することもできる。また、大抵のプログラミング言語には実行時エラーがあり、 実行時エラー は処理が中断するだろう。

最もシンプルなパターンであり、特に予約後を用いたパターンはデッドコードであることが分かりやすい。そのため、既存コードで目にする機会は多くはないだろう。しかし、「goto fail bug4」 も本パターンであり、バグを見逃さないためにも、きちんと対処する必要がある。

🧪return後のコード

🔖 中断を利用するパターン
👼 中断コード削除 ネスト修正による中断コードの移動
🧟 goto文のラベルによるジャンプ ホイスティング
after_return.rb
return
puts 'Am I dead?'

return は、多くのプログラミング言語で、処理を呼出元に戻すために使われる。関数において使われることが多いが、スクリプト言語ではトップレベルのスクリプト環境でも実行できるものもある。

[🔖中断を利用するパターン] である。return 後に書かれているコードは、基本的にはデッドコードとなる。デバッグのために使われることもある。

  • デバッグのために残した return が残ってしまった。 -> 👼中断コード削除
  • if 文等の条件内を想定していたが、ネストを誤った。 -> 👼ネスト修正による中断コードの移動
  • マージ等で return 文が重複した。 -> 👼中断コード削除
  • return 追加による修正後、不要なコードを消さなかった。 -> 🛐供養
  • 削除対象に goto 文のラベルが記載されている。 -> 🧟goto文のラベルによるジャンプ
  • 削除対象にホイスティング対象が記載されている。 -> 🧟ホイスティング

🔧Python

🔧Python -> 🆗実行可, 🔩flake8 -> 🆗検知無

トップレベル(モジュール) では return を使えない。

after_return.py
def f():
    return
    print("Am I dead?")


f()

$ # コード実行
$ python src/after_return.py
$ # flake8
$ flake8 src/after_return.py
$ 

🔧Ruby

🔧Ruby -> ⚠警告有, 🔩rubocop -> ⚠検知有

トップレベルでも return が可能。

after_return.rb
return
puts 'Am I dead?'

$ # コード実行
$ ruby src/after_return.rb
$ # コンパイル(Syntaxチェック&警告確認)
$ ruby -wc src/after_return.rb
src/after_return.rb:2: warning: statement not reached
Syntax OK
$ # rubocop
$ rubocop src/after_return.rb
Inspecting 1 file
W

Offenses:

src/after_return.rb:2:1: W: Lint/UnreachableCode: Unreachable code detected.
puts 'Am I dead?'
^^^^^^^^^^^^^^^^^

1 file inspected, 1 offense detected
$ 

🔧JavaScript

🔧JavaScript -> 🆗実行可, 🔩eslint -> ⚠検知有

🔧node.js でトップレベルの return は実行可能。しかし、🔩eslint で、 error Parsing error: 'return' outside of function が発生するため、関数内で return するコードを示す。

after_return.js
(() => {
    return;
    console.log("Am I dead?");
})()
$ # コード実行
$ node src/after_return.js
$ # eslint
$ eslint src/after_return.js

/app/javascript/src/after_return.js
  3:5  error  Unreachable code  no-unreachable

✖ 1 problem (1 error, 0 warnings)

$ 

🔧Java

🔧Java -> 🚫実行不可

AfterReturn.java
public class AfterReturn {
    public static void main(String[] args) {
        return;
        System.out.println("Am I dead?");
    }
}
$ # コード実行
$ java src/main/java/AfterReturn.java 
src/main/java/AfterReturn.java:5: error: unreachable statement
        System.out.println("Am I dead?");
        ^
1 error
error: compilation failed
$ 

🔧Go

🔧Go -> ⚠検知有

after_return.go
package main

import "fmt"

func main() {
    return
    fmt.Println("Am I dead?")
}

$ # コード実行
$ go run src/after_return.go
$ # 静的解析(標準ツール)実行
$ go vet src/after_return.go 
# command-line-arguments
src/after_return.go:7:2: unreachable code
$ 

👼ネスト修正による中断コードの移動

🔖中断を利用するパターン🔖終らない処理を利用するパターン で利用できるレイズである。

構文木において、 return などの中断を伴う処理が、前要素ではなくなるように、インデントや中括弧を追加することで、後続の処理を復活させる。

after_return_with_if.py
 import sys


 def main():
     if len(sys.argv) > 1:
         print("Cannot enter arguments.")
-    return
+        return
     print("Am I dead?")


 main()

以下では、上記の様に前段に分岐処理がある 🧪return後のコード に対する修正のサンプルを示す。

🔧Python

after_return_with_if.py
 import sys


 def main():
     if len(sys.argv) > 1:
         print("Cannot enter arguments.")
-    return
+        return
     print("Am I dead?")


 main()

🔧Ruby

after_return_with_if.rb
 if ARGV.size.positive?
   puts('Cannot enter arguments.')
+  return
 end
-return
 puts 'Am I dead?'

🔧JavaScript

after_return_with_if.js
 (() => {
     if (process.argv.length > 2) {
         console.log("Cannot enter arguments.");
+        return
     }
-    return
     console.log("Am I dead?");
 })()

🔧Java

AfterReturnWithIf.java
 public class AfterReturnWithIf {
     public static void main(String[] args) {
         if (args.length > 0) {
             System.out.println("Cannot enter arguments.");
+            return;
         }
-        return;
         System.out.println("Am I dead?");
     }
 }

🔧Go

after_return_with_if.go
 package main

 import (
    "fmt"
    "os"
 )

 func main() {
    if len(os.Args) > 1 {
        fmt.Println("Cannot enter arguments.")
+       return
    }
-   return
    fmt.Println("Am I dead?")
 }

🧟goto文のラベルによるジャンプ

🔖中断を利用するパターン🔖終らない処理を利用するパターン で利用できるゾンビ化である。
goto 先が同一ブロックという制限がない言語であれば、他のパターンでもゾンビ化が可能。

goto 文のラベルをデッドコード中に埋め込みジャンプすることで、ゾンビ化を可能にする。

goto_zombie.go
package main

import (
    "fmt"
    "os"
)

func main() {
    if len(os.Args) > 1 {
        fmt.Println("Cannot enter arguments.")
        goto L
    }
    return
L:
    fmt.Println("Am I dead?")
}

上記は、 🔧Go でのサンプルである。vet(標準ツール)⚠検知可能 である。

🔧Go の場合は goto 先が同一ブロックでなくてはいけないという制限がある

goto_zombie_err1.go
package main

import (
    "fmt"
    "os"
)

func main() {
    if len(os.Args) > 1 {
        fmt.Println("Cannot enter arguments.")
        goto L
    }
    return
    if len(os.Args) > 1 {
    L:
        fmt.Println("Am I dead?")
    }
}

上記のプログラムは、 ネスト内にラベルがあり 🚫実行不可 である。

$ # コード実行
$ go run src/zombie/goto_zombie_err1.go 
# command-line-arguments
src/zombie/goto_zombie_err1.go:11:8: goto L jumps into block starting at src/zombie/goto_zombie_err1.go:14:22
$

また、 未使用のラベルも定義できない

goto_zombie_err2.go
package main

import "fmt"

func main() {
    goto A
A:
    goto C
B:
    fmt.Println("Am I dead?")
    goto C
C:
    return
}

上記のプログラムは、 未使用なラベルがあるため 🚫実行不可 である。

$ # コード実行
$ go run src/zombie/goto_zombie_err2.go 
# command-line-arguments
src/zombie/goto_zombie_err2.go:9:1: label B defined and not used
$

上記のような制約も考慮すれば、 可読性を損なわないため、分別のあるプログラミング だと主張 する人もいるだろう。

しかし、以下のプログラムを見て欲しい。

goto_zombie_multi.go
package main

import "fmt"

func main() {
    goto A
A:
    goto D
B:
    fmt.Println("Am I dead?")
    goto C
C:
    fmt.Println("Am I dead?")
    goto B
D:
    return
}

上記プログラムのラベルBとラベルCが実行されることはないが、🆗実行可 であり、vet(標準ツール) でも 🆗検知無 である。

本書では、ラベルによるジャンプでしか到達できないコードはデッドコード として扱い、goto 文のラベルによるジャンプをゾンビ化 として扱う。

goto 文を扱える言語は多くない。本書の対象範囲では、 🔧Go のみである。

🧟ホイスティング

ホイスティング は、ほとんどのデッドコードパターンで利用できる。

任意の文を実行することは出来ないが、定義の巻き上げに伴い、外側の変数を隠蔽し挙動を替えることが可能だ。以下のコードの挙動を考えて欲しい。

hoisting_zombie.js
var d = "Am I dead?";
(() => {
    if (d) {
        console.log(d);
    }
    return;
    var d = "I am zombie!";
})()

上記の実行結果は以下のようになる。

$ # コード実行
$ node src/zombie/hoisting_zombie.js
$ 

コンソールには何も出力されない

また、🔩eslint では以下の通り Unreachable code が出力される。

$ # eslint
$ eslint src/zombie/hoisting_zombie.js 

/app/javascript/src/zombie/hoisting_zombie.js
  1:5  error  'd' is assigned a value but never used  no-unused-vars
  7:5  error  Unreachable code                        no-unreachable

✖ 2 problems (2 errors, 0 warnings)

$ 

🔩eslintUnreachable code を信じて、以下のようにコードを修正すると、挙動が変わるだろう。

hoisting_zombie_2.js
var d = "Am I dead?";
(() => {
    if (d) {
        console.log(d);
    }
    return;
})()
$ # コード実行
$ node src/zombie/hoisting_zombie_2.js 
Am I dead?
$ 

コンソールには「Am I dead?」と出力された

つまり、ゾンビを殺すことで、ゾンビとして復活させるコードである。

原理は知っていれば簡単である。 ホイスティング は、定義のみを巻き上げ、初期化は巻き上げない 6 という理解があれば十分だろう。

なお、本書では以下の様な function によるホイスティングはデッドコードとして扱わない。

hoisting_function.js
var d = "Am I dead?";
(() => {
    if (d) {
        console.log(d);
    }
    return;
    function d() {
        console.log("I am zombie!");
    }
})()

宣言であることが明確であり、🔩eslint でも Unreachable code としては扱われないためだ。

$ # コード実行
$ node src/zombie/hoisting_function.js 
[Function: d]
$ # eslint
$ eslint src/zombie/hoisting_function.js 

/app/javascript/src/zombie/hoisting_function.js
  1:5  error  'd' is assigned a value but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

$ 

ホイスティング を行う言語は多くない。本書の対象範囲では、 🔧JavaScript のみである。また、🔧JavaScript においても var を使用せず、 let, const を使用すれば回避策になるだろう。


当初Qiitaの記事として書こうと思っていたのですが、ボリュームが多くシリーズものにもなるため、Zennの本(無料)として公開することにしました。
他のデッドコードレシピや復活の呪文が気になる方は、本をチェックしてみて下さい。

また、12/24に 【Ruby】🏡THE HOUSE OF THE DEAD CODE🧟🧟‍♀️🧟‍♂️ という記事 (Qiitaのみ) を書く予定です。

みなさんで 闇の魔術に対する防衛術 Advent Calendar 2020 を盛り上げましょう

43
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
22