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. 字句解析 token_get_all('<?php echo; ?>'); [ T_OPEN_TAG, T_ECHO, T_WHITESPACE, T_CLOSE_TAG ]

    単なる文字列である ソースコードから 「トークン列」を生成する
  2. 構文解析 \ast\parse_code( '<?php echo "Hello, World!"; ?>', 60 ); AST_STMT_LIST

    0: AST_ECHO expr: "Hello, World!" 得られたトークン列から 抽象構文木(AST)を得る
  3. 抽象構文木の例 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 );
  4. PHPの有名な構文解析器 • nikic/PHP-Parser ◦ たぶん一番有名、全部PHPで書かれている ◦ PHPStanなどで採用 • nikic/php-ast ◦

    内部で生成されたASTを取得するextension(早い) ◦ Phanなどで採用 今日はこっちを話します
  5. 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 );
  6. 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()を愚直に探す
  7. 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 ...
  8. 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 ...
  9. 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 ...
  10. 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: ... 部分的な構造は同じ
  11. 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) { ... } }
  12. 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: ...
  13. 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: ...
  14. Visitorの実装 class Visitor { public function enterNode(Node $node) { if

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

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

    foreach ($node->children as $child) { if ($child instanceof Node) { $this->traverse($child); } } } }
  17. 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を呼び出す
  18. 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がある場合、さらに走査する 再帰で実装できる