Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PHPを検査するPHPを書く / Write PHP inspection by PHP

PHPを検査するPHPを書く / Write PHP inspection by PHP

PHPカンファレンス2018 Track7

Kazuma Watanabe

December 15, 2018
Tweet

More Decks by Kazuma Watanabe

Other Decks in Technology

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  4. PHPを検査するとはなにか

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  16. ちょっとまって

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  22. 正規表現は難しい
    文字列リテラルを区別できない
    'var_dump is debug code';

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. 構文解析器を使う
    構文解析で得られる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"

    View full-size slide

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

    View full-size slide

  30. 抽象構文木の例
    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
    );

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  37. 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
    );

    View full-size slide

  38. 抽象構文木を走査する

    View full-size slide

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

    View full-size slide

  40. 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()を愚直に探す

    View full-size slide

  41. 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
    ...

    View full-size slide

  42. 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
    ...

    View full-size slide

  43. 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
    ...

    View full-size slide

  44. 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: ...
    部分的な構造は同じ

    View full-size slide

  45. 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) { ... }
    }

    View full-size slide

  46. 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: ...

    View full-size slide

  47. 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: ...

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  54. 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を呼び出す

    View full-size slide

  55. 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がある場合、さらに走査する
    再帰で実装できる

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  66. Thank you!
    https://sider.review/

    View full-size slide