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
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
1
210
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
480
コンテナをSpot Fleetで起動するという選択肢
wata727
2
1.2k
エンジニア向けSaaSを支えるInfrastructure as Code
wata727
5
2.5k
SideCIのインフラ構築を自動化した話
wata727
1
2.2k
Other Decks in Technology
See All in Technology
AIとペアプロを始める。人とのペアプロをやめる。ペアプロの良さを改めて知る。もっと好きになった。 / Rediscovering Pair Programming
honyanya
1
350
GoにおけるFFIのこれまでとこれから
goccy
5
2.5k
AI時代のAPI品質を支えるガードレール / API Guardrails for API quality in the AI era
yokawasa
1
220
module Synths; end
asonas
1
130
アプリをもっと"iOSアプリっぽく"する小さな工夫 / Small Touches That Make Your App Feel More Like an iOS App
matsuji
1
630
Gitは怖い?共有ワークスペースから始めるSnowflakeチーム開発
coco_se
0
180
WAF 運用改善の承認サイクル/SRE_BizReach_MIXI_1
visional_engineering_and_design
2
570
OpenTelemetry eBPF Instrumentationの舞台裏 / Behind the Scenes of OpenTelemetry eBPF Instrumentation
ymotongpoo
4
1.1k
プロダクト思考 × 基盤思考を AIで実現する Compound Engineering
tkc66buzz
1
330
Screen Lens - 今見てる画面を翻訳する
komagata
0
260
Code4Lib JAPANカンファレンス2026 開会挨拶 / Code4Lib JAPAN Conference 2026: Opening Remarks
ykiyota
0
340
DEFCON_CHV_CTF_Write-up.pdf
bata_24
0
140
Featured
See All Featured
Applied NLP in the Age of Generative AI
inesmontani
PRO
4
2.4k
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
320
A Modern Web Designer's Workflow
chriscoyier
699
190k
Designing for Timeless Needs
cassininazir
1
470
Embracing the Ebb and Flow
colly
88
5.2k
Marketing Yourself as an Engineer | Alaka | Gurzu
gurzu
0
300
The World Runs on Bad Software
bkeepers
PRO
72
12k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
38
3k
Build The Right Thing And Hit Your Dates
maggiecrowley
39
3.4k
Odyssey Design
rkendrick25
PRO
2
800
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.6k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
141
35k
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/