Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
PHPを検査するPHPを書く / Write PHP inspection by PHP
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
Kazuma Watanabe
December 15, 2018
Technology
2.4k
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
PHPを検査するPHPを書く / Write PHP inspection by PHP
PHPカンファレンス2018 Track7
Kazuma Watanabe
December 15, 2018
More Decks by Kazuma Watanabe
See All by Kazuma Watanabe
Terraform言語の静的解析 / static analysis of Terraform language
wata727
1
160
SmartHRにおけるBiTemporal Data Modelの実践のその後 / After the practice of BiTemporal Data Model in SmartHR
wata727
1
3.9k
快適なコードレビューを目指して / For a comfortable code review
wata727
1
710
現実世界でのコンテナの運び方
wata727
3
1.2k
Lintの付き合い方とPahoutのご紹介
wata727
0
210
Querlyで始めるコードレビューの自動化
wata727
2
480
コンテナをSpot Fleetで起動するという選択肢
wata727
2
1.1k
エンジニア向けSaaSを支えるInfrastructure as Code
wata727
5
2.5k
SideCIのインフラ構築を自動化した話
wata727
1
2.2k
Other Decks in Technology
See All in Technology
事業会社における 機械学習・推薦システム技術の活用事例と必要な能力 / ml-recsys-in-layerx-wantedly-2026
yuya4
0
160
iOS アプリの「これって不具合ですか?」を AI に調べてもらう
miichan
0
140
AIチャットの改善から見えた、良いAI体験とは / What Constitutes a Good AI Experience: Insights from Improving AI Chat
kubode
0
120
AIチャット検索改善の3週間
kworkdev
PRO
2
180
コミュニティの有益性 ~JAWS Days 2026 での体験を通して~ / The Benefits of a Community ~Through My Experience at JAWS Days 2026~
seike460
PRO
0
270
徹底討論!ECS vs EKS!
daitak
3
1.7k
2026 AI Memory Architecture
nagatsu
0
320
SteampipeとExcel Power QueryでAWS構成定義書の作成を自動化する
jhashimoto
0
180
LayerX コーポレートエンジニアリング室におけるサプライチェーンセキュリティへの取り組み / Supply Chain Security at LayerX Corporate Engineering
yuyatakeyama
3
840
螺旋型キャリアの生存戦略 / kinoko-conf2026
rakus_dev
1
1k
「軸足」は 固定しなくていい - 熱量と強みで描く、しなやかなキャリアの形
kakehashi
PRO
1
270
【FinOps】データドリブンな意思決定を目指して
z63d
0
370
Featured
See All Featured
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
630
The Language of Interfaces
destraynor
162
27k
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
240
GraphQLとの向き合い方2022年版
quramy
50
15k
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
1
260
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
35
3.5k
AI Search: Where Are We & What Can We Do About It?
aleyda
0
7.6k
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
HTML-Aware ERB: The Path to Reactive Rendering @ RubyCon 2026, Rimini, Italy
marcoroth
2
240
What’s in a name? Adding method to the madness
productmarketing
PRO
24
4.1k
Crafting Experiences
bethany
1
190
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
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/