Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
PHPを検査するPHPを書く / Write PHP inspection by PHP
Kazuma Watanabe
December 15, 2018
Technology
1
1.5k
PHPを検査するPHPを書く / Write PHP inspection by PHP
PHPカンファレンス2018 Track7
Kazuma Watanabe
December 15, 2018
Tweet
Share
More Decks by Kazuma Watanabe
See All by Kazuma Watanabe
SmartHRにおけるBiTemporal Data Modelの実践のその後 / After the practice of BiTemporal Data Model in SmartHR
wata727
1
800
快適なコードレビューを目指して / For a comfortable code review
wata727
1
460
現実世界でのコンテナの運び方
wata727
3
930
Lintの付き合い方とPahoutのご紹介
wata727
0
130
Querlyで始めるコードレビューの自動化
wata727
2
400
コンテナをSpot Fleetで起動するという選択肢
wata727
2
960
エンジニア向けSaaSを支えるInfrastructure as Code
wata727
5
2k
SideCIのインフラ構築を自動化した話
wata727
1
1.9k
Other Decks in Technology
See All in Technology
メドレー エンジニア採用資料/ Medley Engineer Guide
medley
3
5k
目指せCoverage100%! AutoScale環境におけるSavings Plans購入戦略 / JAWS-UG_SRE_Coverage
taishin
0
460
SmartHRからOktaへのSCIM連携で作り出すHRドリブンのアカウント管理
jousysmiler
1
110
400種類のWeb APIをサポートしているデータパイプラインツールにおけるWeb APIとの共存戦略
cdataj
0
160
証明書って何だっけ? 〜AWSの中間CA移行に備える〜
minorun365
3
2k
WebLogic Server for OCI 概要
oracle4engineer
PRO
3
860
S3とCloudWatch Logsの見直しから始めるコスト削減 / Cost saving S3 and CloudWatch Logs
shonansurvivors
0
210
OCI DevOps 概要 / OCI DevOps overview
oracle4engineer
PRO
0
480
Airdrop for Open Source Projects
epicsdao
0
500
Startup Studio Sereal / Culture Deck
sereal
0
650
AI Services 概要 / AI Services overview
oracle4engineer
PRO
0
160
NGINXENG JP#2 - 3-NGINX Plus・プロダクトのアップデート
hiropo20
0
200
Featured
See All Featured
Three Pipe Problems
jasonvnalue
89
8.9k
It's Worth the Effort
3n
177
26k
Build your cross-platform service in a week with App Engine
jlugia
221
17k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
270
12k
Code Review Best Practice
trishagee
50
11k
Designing the Hi-DPI Web
ddemaree
273
32k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
152
13k
Git: the NoSQL Database
bkeepers
PRO
419
60k
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
15
1.2k
Build The Right Thing And Hit Your Dates
maggiecrowley
22
1.4k
Become a Pro
speakerdeck
PRO
6
3.2k
From Idea to $5000 a Month in 5 Months
shpigford
374
44k
Transcript
PHPを検査するPHPを書く PHPカンファレンス2018 @wata727
自己紹介 • @wata727 • Sider, Inc. • GitHub: @wata727 •
Twitter: @wata727_
宣伝
None
Sider • GitHubと連携するコードレビュー支援SaaS • プルリクエストを解析し、結果をGitHubに報告 • ベストプラクティスやプロジェクト特有のルールに 関するレビューを自動化して、繰り返しを防ぐ
本題
PHPを検査するとはなにか
いろいろありますが... 今回は主に 「特定のよくある間違いを見つけること」 に焦点を当てます
例えばこんなコード in_array()の第三引数を省略すると「緩やかな比較」 になる(デフォルト値がfalse) in_array("string", [0, 1]); // => true in_array("string",
[0, 1], true); // => false
例えばこんなコード PHP 5.4から配列をarray()以外で宣言可能に $a = array(1, 2); $b = [1,
2]; // PHP 5.4以降
例えばこんなコード arrayのキーの重複(重複した場合、後から宣言され た要素によって上書きされる) $a = [ "201403" => 0.5, "201404"
=> 0.5, "201404" => 0.8, // "201404"が2つある! "201405" => 0,8 ]; "201404" "201404"
今回はこれらを 機械的にチェックする方法 について話します
大体の問題は解決されている • in_array()の第三引数 ◦ https://github.com/phpstan/phpstan-strict-rules • arrayの短縮構文 ◦ Generic.Arrays.DisallowLongArraySyntaxSniff •
arrayのキー重複 ◦ Phan, PHPStan, Pahoutなど
ではなぜやるのか? • 意外と簡単 • PHPそのものに詳しくなれる • 既存のLinterに機能追加やバグ修正を送れる • オリジナルのLinterを作れる •
楽しい!
ソースコードの解析ステップ
ソースコードの解析ステップ 1. 字句解析 2. 構文解析 3. 抽象構文木の走査 4. 結果の出力
字句解析 token_get_all('<?php echo; ?>'); [ T_OPEN_TAG, T_ECHO, T_WHITESPACE, T_CLOSE_TAG ]
単なる文字列である ソースコードから 「トークン列」を生成する
構文解析 \ast\parse_code( '<?php echo "Hello, World!"; ?>', 60 ); AST_STMT_LIST
0: AST_ECHO expr: "Hello, World!" 得られたトークン列から 抽象構文木(AST)を得る
ちょっとまって
なんでこんなん必要なの?
素朴な疑問 • 単なる文字列パターンの認識なら正規表現でい いんじゃないの? • トークン列への変換とか、構文解析とかやる必要 なくない?
正規表現は難しい var_dump()があるかどうかチェックする $code = file_get_contents("input.php"); if (preg_match("/var_dump/", $code)) { echo
"Found!"; }
正規表現は難しい コメントを区別できない // var_dump($obj) $obj = somefunc();
正規表現は難しい コメントを区別できない // var_dump($obj) $obj = somefunc(); preg_match(/^[^(\/\/)]+var_dump/, $code);
正規表現は難しい 文字列リテラルを区別できない <?php 'var_dump is debug code';
正規表現は難しい 文字列リテラルを区別できない preg_match(/^[^(\/\/|'|\")]+var_dump/, $code); <?php 'var_dump is debug code';
正規表現は難しい 類似した関数名を区別できない $dumper = var_dumper(); $dumper->dump();
正規表現は難しい 類似した関数名を区別できない preg_match(/^[^(\/\/|'|\")]+var_dump\(/,$code); $dumper = var_dumper(); $dumper->dump();
無理
字句解析器を使う 字句解析器は先に書いた例を区別できる var_dump($obj); // var_dump($obj); var_dumper(); T_STRING (var_dump) T_COMMENT T_STRING
(var_dumper)
意味に無関係なトークンを消す 字句解析で得られたトークンには「実行結果に影響 を与えないトークン」が含まれる var_dump($obj); var_dump( $obj ); T_STRING (var_dump) T_VARIABLE
($obj) T_STRING (var_dump) T_WHITESPACE T_VARIABLE ($obj) T_WHITESPACE
構文解析器を使う 構文解析で得られる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"
AST?
抽象構文木 (Abstract Syntax Tree) • 意味の変わらない不要なトークンを落とし、ソース コードを木構造で表現したもの
抽象構文木の例 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 );
抽象構文木の例 AST_CALL AST_NAME AST_ARG_LIST “string” AST_ARRAY AST_CONST in_array [0, 1]
true
抽象構文木の例 AST_CALL AST_NAME AST_ARG_LIST “string” AST_ARRAY AST_CONST in_array [0, 1]
true in_array("string", [0, 1], true);
抽象構文木の例 AST_CALL AST_NAME AST_ARG_LIST “string” AST_ARRAY AST_CONST in_array [0, 1]
true in_array("string", [0, 1], true);
抽象構文木の例 AST_CALL AST_NAME AST_ARG_LIST “string” AST_ARRAY AST_CONST in_array [0, 1]
true in_array("string", [0, 1], true);
PHPの有名な構文解析器 • nikic/PHP-Parser ◦ たぶん一番有名、全部PHPで書かれている ◦ PHPStanなどで採用 • nikic/php-ast ◦
内部で生成されたASTを取得するextension(早い) ◦ Phanなどで採用
PHPの有名な構文解析器 • nikic/PHP-Parser ◦ たぶん一番有名、全部PHPで書かれている ◦ PHPStanなどで採用 • nikic/php-ast ◦
内部で生成されたASTを取得するextension(早い) ◦ Phanなどで採用 今日はこっちを話します
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 );
抽象構文木を走査する
どうやってASTを検査するのか • 得られたASTは単なるast\Nodeインスタンス • Nodeがさらに子ノードを持つ • 適当にforeachして、中身のNodeの種類に応じて if文を書けばいいんじゃない?
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()を愚直に探す
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 ...
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 ...
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 ...
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: ... 部分的な構造は同じ
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) { ... } }
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: ...
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: ...
他のケースは? • 例えば elseの中にin_array()があったら? • 関数の引数にin_array()があったら? • try...catchの中にin_array()があったら? • などなど...
無理
どうすればいいのか • in_array()の含まれるASTの構造は様々 • 構造に関係なく、AST_CALLのNodeが来たとき に検査したい
Visitorクラスの導入 • データ構造を走査するためのクラスと、ある特定 の要素を処理するクラスを分離する • Nodeを検査するロジックを実装したクラスを用意 する(Visitor) • 子ノードなどを走査して、Nodeが来るたびに Visitorを適用する(Traverser)
Visitorの実装 class Visitor { public function enterNode(Node $node) { if
($node->kind === AST_CALL) { $this->processCall($node); } } }
Visitorの実装 class Visitor { public function enterNode(Node $node) { if
($node->kind === AST_CALL) { $this->processCall($node); } } } 単一のNodeのみを考慮する (子ノードなどの構造は考慮しなくて良い)
Traverserの実装 class Traverser { public function traverse(Node $node) { $this->visitor->enterNode($node);
foreach ($node->children as $child) { if ($child instanceof Node) { $this->traverse($child); } } } }
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を呼び出す
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がある場合、さらに走査する 再帰で実装できる
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
深さ優先探索のイメージ AST_IF AST_IF_ELEM AST_CALL AST_STMT_LIST AST_CALL
実装例 • https://github.com/wata727/pahout • 処理の流れ ◦ bin/pahout ◦ src/Command/Check.php ◦
src/Pahout.php ◦ src/Formatter.php
Thank you! https://sider.review/