Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
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.5k
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
2
220
SmartHRにおけるBiTemporal Data Modelの実践のその後 / After the practice of BiTemporal Data Model in SmartHR
wata727
1
3.9k
快適なコードレビューを目指して / For a comfortable code review
wata727
1
720
現実世界でのコンテナの運び方
wata727
3
1.2k
Lintの付き合い方とPahoutのご紹介
wata727
0
220
Querlyで始めるコードレビューの自動化
wata727
2
490
コンテナをSpot Fleetで起動するという選択肢
wata727
2
1.2k
エンジニア向けSaaSを支えるInfrastructure as Code
wata727
5
2.5k
SideCIのインフラ構築を自動化した話
wata727
1
2.3k
Other Decks in Technology
See All in Technology
白金鉱業Meetup Vol.25 アウトカムが二値のデータに対するCausal Impact
brainpadpr
0
270
ADKで始める業務改善 - AIエージェント開発時の考えと設計
harappa80
2
250
なぜ「決定性」が 決定的に重要なのか? Durable Execution 基盤の数理的理解 #serverlessjp / ServerlessDays Tokyo 2026
ytaka23
3
1.1k
アプリログインとWeb認証基盤をつなぐ ASWebAuthenticationSession 作法
shimastripe
1
370
What the customer really needed
kawaguti
PRO
3
220
人間はどの意思決定を手放せるのか
kawasima
15
7.9k
サーバーレスをどこまで使う? WebRTC対戦ゲームで選んだVPSとの共存設計
kaidouji85
0
300
Cloudflare Workers 向けアプリを C# で構築する ~WASM Native AOT への道~
nenonaninu
1
920
AIエージェントの権限管理 3: Agentic RAG の Fine grained access control 編
ren8k
0
180
Claude in Chrome 入門 / Introduction to Claude in Chrome
cielo1985
0
880
バイブコーディング時代のWebアプリ開発入門~Cloud Runで学ぶセキュアなビルドとデプロイ
waiwai2111
1
140
AIは推し活である。
kurazuuuuuu
1
930
Featured
See All Featured
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
69
65k
Making the Leap to Tech Lead
cromwellryan
135
10k
30 Presentation Tips
portentint
PRO
1
400
Building Applications with DynamoDB
mza
96
7.2k
AI in Enterprises - Java and Open Source to the Rescue
ivargrimstad
0
1.5k
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
3k
KATA
mclloyd
PRO
35
15k
Chasing Engaging Ingredients in Design
codingconduct
0
320
GraphQLとの向き合い方2022年版
quramy
50
15k
Impact Scores and Hybrid Strategies: The future of link building
tamaranovitovic
0
440
Building a A Zero-Code AI SEO Workflow
portentint
PRO
0
730
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
50
10k
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/