Slide 1

Slide 1 text

PHPを検査するPHPを書く PHPカンファレンス2018 @wata727

Slide 2

Slide 2 text

自己紹介 ● @wata727 ● Sider, Inc. ● GitHub: @wata727 ● Twitter: @wata727_

Slide 3

Slide 3 text

宣伝

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

Sider ● GitHubと連携するコードレビュー支援SaaS ● プルリクエストを解析し、結果をGitHubに報告 ● ベストプラクティスやプロジェクト特有のルールに 関するレビューを自動化して、繰り返しを防ぐ

Slide 6

Slide 6 text

本題

Slide 7

Slide 7 text

PHPを検査するとはなにか

Slide 8

Slide 8 text

いろいろありますが... 今回は主に 「特定のよくある間違いを見つけること」 に焦点を当てます

Slide 9

Slide 9 text

例えばこんなコード in_array()の第三引数を省略すると「緩やかな比較」 になる(デフォルト値がfalse) in_array("string", [0, 1]); // => true in_array("string", [0, 1], true); // => false

Slide 10

Slide 10 text

例えばこんなコード PHP 5.4から配列をarray()以外で宣言可能に $a = array(1, 2); $b = [1, 2]; // PHP 5.4以降

Slide 11

Slide 11 text

例えばこんなコード arrayのキーの重複(重複した場合、後から宣言され た要素によって上書きされる) $a = [ "201403" => 0.5, "201404" => 0.5, "201404" => 0.8, // "201404"が2つある! "201405" => 0,8 ]; "201404" "201404"

Slide 12

Slide 12 text

今回はこれらを 機械的にチェックする方法 について話します

Slide 13

Slide 13 text

大体の問題は解決されている ● in_array()の第三引数 ○ https://github.com/phpstan/phpstan-strict-rules ● arrayの短縮構文 ○ Generic.Arrays.DisallowLongArraySyntaxSniff ● arrayのキー重複 ○ Phan, PHPStan, Pahoutなど

Slide 14

Slide 14 text

ではなぜやるのか? ● 意外と簡単 ● PHPそのものに詳しくなれる ● 既存のLinterに機能追加やバグ修正を送れる ● オリジナルのLinterを作れる ● 楽しい!

Slide 15

Slide 15 text

ソースコードの解析ステップ

Slide 16

Slide 16 text

ソースコードの解析ステップ 1. 字句解析 2. 構文解析 3. 抽象構文木の走査 4. 結果の出力

Slide 17

Slide 17 text

字句解析 token_get_all(''); [ T_OPEN_TAG, T_ECHO, T_WHITESPACE, T_CLOSE_TAG ] 単なる文字列である ソースコードから 「トークン列」を生成する

Slide 18

Slide 18 text

構文解析 \ast\parse_code( '', 60 ); AST_STMT_LIST 0: AST_ECHO expr: "Hello, World!" 得られたトークン列から 抽象構文木(AST)を得る

Slide 19

Slide 19 text

ちょっとまって

Slide 20

Slide 20 text

なんでこんなん必要なの?

Slide 21

Slide 21 text

素朴な疑問 ● 単なる文字列パターンの認識なら正規表現でい いんじゃないの? ● トークン列への変換とか、構文解析とかやる必要 なくない?

Slide 22

Slide 22 text

正規表現は難しい var_dump()があるかどうかチェックする $code = file_get_contents("input.php"); if (preg_match("/var_dump/", $code)) { echo "Found!"; }

Slide 23

Slide 23 text

正規表現は難しい コメントを区別できない // var_dump($obj) $obj = somefunc();

Slide 24

Slide 24 text

正規表現は難しい コメントを区別できない // var_dump($obj) $obj = somefunc(); preg_match(/^[^(\/\/)]+var_dump/, $code);

Slide 25

Slide 25 text

正規表現は難しい 文字列リテラルを区別できない

Slide 26

Slide 26 text

正規表現は難しい 文字列リテラルを区別できない preg_match(/^[^(\/\/|'|\")]+var_dump/, $code);

Slide 27

Slide 27 text

正規表現は難しい 類似した関数名を区別できない $dumper = var_dumper(); $dumper->dump();

Slide 28

Slide 28 text

正規表現は難しい 類似した関数名を区別できない preg_match(/^[^(\/\/|'|\")]+var_dump\(/,$code); $dumper = var_dumper(); $dumper->dump();

Slide 29

Slide 29 text

無理

Slide 30

Slide 30 text

字句解析器を使う 字句解析器は先に書いた例を区別できる var_dump($obj); // var_dump($obj); var_dumper(); T_STRING (var_dump) T_COMMENT T_STRING (var_dumper)

Slide 31

Slide 31 text

意味に無関係なトークンを消す 字句解析で得られたトークンには「実行結果に影響 を与えないトークン」が含まれる var_dump($obj); var_dump( $obj ); T_STRING (var_dump) T_VARIABLE ($obj) T_STRING (var_dump) T_WHITESPACE T_VARIABLE ($obj) T_WHITESPACE

Slide 32

Slide 32 text

構文解析器を使う 構文解析で得られるASTはこれらを区別しない var_dump($obj); var_dump( $obj ); AST_STMT_LIST 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "var_dump" args: AST_ARG_LIST 0: AST_VAR name: "obj"

Slide 33

Slide 33 text

AST?

Slide 34

Slide 34 text

抽象構文木 (Abstract Syntax Tree) ● 意味の変わらない不要なトークンを落とし、ソース コードを木構造で表現したもの

Slide 35

Slide 35 text

抽象構文木の例 AST_STMT_LIST 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "in_array" args: AST_ARG_LIST 0: "string" 1: AST_ARRAY flags: ARRAY_SYNTAX_SHORT (3) 0: AST_ARRAY_ELEM flags: 0 value: 0 key: null 1: AST_ARRAY_ELEM flags: 0 value: 1 key: null 2: AST_CONST name: AST_NAME flags: NAME_NOT_FQ (1) name: "true" in_array( "string", [0, 1], true );

Slide 36

Slide 36 text

抽象構文木の例 AST_CALL AST_NAME AST_ARG_LIST “string” AST_ARRAY AST_CONST in_array [0, 1] true

Slide 37

Slide 37 text

抽象構文木の例 AST_CALL AST_NAME AST_ARG_LIST “string” AST_ARRAY AST_CONST in_array [0, 1] true in_array("string", [0, 1], true);

Slide 38

Slide 38 text

抽象構文木の例 AST_CALL AST_NAME AST_ARG_LIST “string” AST_ARRAY AST_CONST in_array [0, 1] true in_array("string", [0, 1], true);

Slide 39

Slide 39 text

抽象構文木の例 AST_CALL AST_NAME AST_ARG_LIST “string” AST_ARRAY AST_CONST in_array [0, 1] true in_array("string", [0, 1], true);

Slide 40

Slide 40 text

PHPの有名な構文解析器 ● nikic/PHP-Parser ○ たぶん一番有名、全部PHPで書かれている ○ PHPStanなどで採用 ● nikic/php-ast ○ 内部で生成されたASTを取得するextension(早い) ○ Phanなどで採用

Slide 41

Slide 41 text

PHPの有名な構文解析器 ● nikic/PHP-Parser ○ たぶん一番有名、全部PHPで書かれている ○ PHPStanなどで採用 ● nikic/php-ast ○ 内部で生成されたASTを取得するextension(早い) ○ Phanなどで採用 今日はこっちを話します

Slide 42

Slide 42 text

php-astが生成するASTの例 object(ast\Node)#1 (4) { ["kind"]=> int(132) ["flags"]=> int(0) ["lineno"]=> int(1) ["children"]=> array(1) { [0]=> object(ast\Node)#2 (4) { … } } } in_array( "string", [0, 1], true );

Slide 43

Slide 43 text

抽象構文木を走査する

Slide 44

Slide 44 text

どうやってASTを検査するのか ● 得られたASTは単なるast\Nodeインスタンス ● Nodeがさらに子ノードを持つ ● 適当にforeachして、中身のNodeの種類に応じて if文を書けばいいんじゃない?

Slide 45

Slide 45 text

foreach ($root->children as $node) { if ($node instanceof Node) { if ($node->kind === AST_CALL && $node->children['expr']->kind === AST_NAME && $node->children['expr']->children['name'] === "in_array") { echo "Found!"; } } } in_array()を愚直に探す

Slide 46

Slide 46 text

foreach ($root->children as $node) { if ($node instanceof Node) { if ($node->kind === AST_CALL && $node->children['expr']->kind === AST_NAME && $node->children['expr']->children['name'] === "in_array") { echo "Found!"; } } } in_array()を愚直に探す AST_STMT_LIST 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "in_array" args: AST_ARG_LIST ...

Slide 47

Slide 47 text

foreach ($root->children as $node) { if ($node instanceof Node) { if ($node->kind === AST_CALL && $node->children['expr']->kind === AST_NAME && $node->children['expr']->children['name'] === "in_array") { echo "Found!"; } } } in_array()を愚直に探す AST_STMT_LIST 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "in_array" args: AST_ARG_LIST ...

Slide 48

Slide 48 text

foreach ($root->children as $node) { if ($node instanceof Node) { if ($node->kind === AST_CALL && $node->children['expr']->kind === AST_NAME && $node->children['expr']->children['name'] === "in_array") { echo "Found!"; } } } in_array()を愚直に探す AST_STMT_LIST 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "in_array" args: AST_ARG_LIST ...

Slide 49

Slide 49 text

if文の中にあると検出できない AST_STMT_LIST 0: AST_IF 0: AST_IF_ELEM cond: AST_CALL expr: ... args: ... stmts: AST_STMT_LIST 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "in_array" args: ... if (condition()) { in_array( "string", [0, 1], true ); } in_array( "string", [0, 1], true ); 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "in_array" args: ... 部分的な構造は同じ

Slide 50

Slide 50 text

foreach ($root->children as $node) { if ($node instanceof Node) { if ($node->kind === AST_CALL && $node->children['expr']->kind === AST_NAME && $node->children['expr']->children['name'] === "in_array") { echo "Found!"; } if ($node->kind === AST_IF) { foreach ($node->children as $if_elem) { ... } } } } if文を考慮する if ($node->kind === AST_IF) { foreach ($node->children as $if_elem) { ... } }

Slide 51

Slide 51 text

foreach ($root->children as $node) { if ($node instanceof Node) { if ($node->kind === AST_CALL && $node->children['expr']->kind === AST_NAME && $node->children['expr']->children['name'] === "in_array") { echo "Found!"; } if ($node->kind === AST_IF) { foreach ($node->children as $if_elem) { ... } } } } if文を考慮する if ($node->kind === AST_IF) { foreach ($node->children as $if_elem) { ... } } AST_STMT_LIST 0: AST_IF 0: AST_IF_ELEM cond: AST_CALL expr: ... args: ... stmts: AST_STMT_LIST 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "in_array" args: ...

Slide 52

Slide 52 text

foreach ($root->children as $node) { if ($node instanceof Node) { if ($node->kind === AST_CALL && $node->children['expr']->kind === AST_NAME && $node->children['expr']->children['name'] === "in_array") { echo "Found!"; } if ($node->kind === AST_IF) { foreach ($node->children as $if_elem) { ... } } } } if文を考慮する if ($node->kind === AST_IF) { foreach ($node->children as $if_elem) { ... } } AST_STMT_LIST 0: AST_IF 0: AST_IF_ELEM cond: AST_CALL expr: ... args: ... stmts: AST_STMT_LIST 0: AST_CALL expr: AST_NAME flags: NAME_NOT_FQ (1) name: "in_array" args: ...

Slide 53

Slide 53 text

他のケースは? ● 例えば elseの中にin_array()があったら? ● 関数の引数にin_array()があったら? ● try...catchの中にin_array()があったら? ● などなど...

Slide 54

Slide 54 text

無理

Slide 55

Slide 55 text

どうすればいいのか ● in_array()の含まれるASTの構造は様々 ● 構造に関係なく、AST_CALLのNodeが来たとき に検査したい

Slide 56

Slide 56 text

Visitorクラスの導入 ● データ構造を走査するためのクラスと、ある特定 の要素を処理するクラスを分離する ● Nodeを検査するロジックを実装したクラスを用意 する(Visitor) ● 子ノードなどを走査して、Nodeが来るたびに Visitorを適用する(Traverser)

Slide 57

Slide 57 text

Visitorの実装 class Visitor { public function enterNode(Node $node) { if ($node->kind === AST_CALL) { $this->processCall($node); } } }

Slide 58

Slide 58 text

Visitorの実装 class Visitor { public function enterNode(Node $node) { if ($node->kind === AST_CALL) { $this->processCall($node); } } } 単一のNodeのみを考慮する (子ノードなどの構造は考慮しなくて良い)

Slide 59

Slide 59 text

Traverserの実装 class Traverser { public function traverse(Node $node) { $this->visitor->enterNode($node); foreach ($node->children as $child) { if ($child instanceof Node) { $this->traverse($child); } } } }

Slide 60

Slide 60 text

Traverserの実装 class Traverser { public function traverse(Node $node) { $this->visitor->enterNode($node); foreach ($node->children as $child) { if ($child instanceof Node) { $this->traverse($child); } } } } Nodeが来るたびにVisitorを呼び出す

Slide 61

Slide 61 text

Traverserの実装 class Traverser { public function traverse(Node $node) { $this->visitor->enterNode($node); foreach ($node->children as $child) { if ($child instanceof Node) { $this->traverse($child); } } } } 子Nodeがある場合、さらに走査する 再帰で実装できる

Slide 62

Slide 62 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 63

Slide 63 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 64

Slide 64 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 65

Slide 65 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 66

Slide 66 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 67

Slide 67 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 68

Slide 68 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 69

Slide 69 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 70

Slide 70 text

深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL

Slide 71

Slide 71 text

実装例 ● https://github.com/wata727/pahout ● 処理の流れ ○ bin/pahout ○ src/Command/Check.php ○ src/Pahout.php ○ src/Formatter.php

Slide 72

Slide 72 text

Thank you! https://sider.review/