PhanによるPHPコード静的解析(公開補訂版)

公開日:

Download PDF

スライドテキスト

Page 1

Phanによる

静的解析

PHPコード
公開補訂版

ピクシブ株式会社
うさみけんた @tadsan

JAPAN phpcon 2016, 3rd November

Page 2

お前誰よ
うさみけんた (@tadsan) / Zonu.EXE

  • GitHub/Packagistでは id:zonuexe
    • 2012年 自宅警備→ピクシブ株式会社
  • モバイルアプリ向けWebAPI開発とか
    • 今年の春からメンテナンスとしてチーム独立

日常的なバグ修正から、アーキテクチャの改善

  • リファクタリングやテストなどいろいろ!

Page 3

おしごと

Page 4

現実に動いている

(ユーザーさんが利用している)

サービスを破壊せずに
コードの健全さを向上したい

Page 5

Page 6

そんなノウハウをギュッと詰め込んだ
PHP大規模開発入門

(連載中)

Page 7

PHP大規模開発入門

Vol.87:

PHPDocでコードの品質を保つ

  • Vol.91:

名前空間とオートローディング

Vol.94:

PHP初心者がハマりがちな落とし穴

……型のキャスト,変数とリファレンス

Vol.95:

PHPの静的解析

  • ……ドキュメントの生成,問題箇所の発見

Page 8

PHP大規模開発入門

Vol.87:

PHPDocでコードの品質を保つ

  • Vol.91:

名前空間とオートローディング

Vol.94:

PHP初心者がハマりがちな落とし穴

……のキャスト,変数とリファレンス

Vol.95:

PHPの静的解析

  • ……ドキュメントの生成,問題箇所の発見

Page 9

おしながき

PHPと型

  • PHPDocとは何か
  • Phanの紹介

Page 10

PHPと型

Page 11

動的型検査

プログラムを動かしながら(←動的)

手さぐりで型を調べる (←型検査)

$a = 13; $b = "2";

echo $a + $b;

$a int $b string
の型は だな、 の型は …

!」

だけど数字だから足しざんできるね

……みたいな茶番を毎回やってる

Page 12

PHPの型

変数に型はないが、値が型を持つ

  • int id
    C言語みたいに のような定義はできない
    • 64 'A'
      厳然とした別の型の値
    • '64' == 64

のような比較が成り立つ設計

  • 関数は型宣言できる (ただし動的検査)

Page 13

PHPの型の種類

整数 浮動小数点数 論理値

int float bool

  • ( ) ( ) ( )
    文字列 配列 リソース

string array resource
( ) ( ) ( )

オブジェクト ヌル値

object null
( ) ( )

array

では のみ宣言に記述できる

  • PHP5

int float string bool
7.0では , , , が記述できる

Page 14

型宣言(PHP7)

PHP7の関数(メソッド・クロージャ含む)は、
引数返り値に型を定義することができる

// PHP 5

function trapezoid($h, $a, $b) {

return $h * ($a + $b) / 2;

}

// PHP 7

function trapezoid(float $h, float $a, float $b): float

{

return $h * ($a + $b) / 2;

}

Page 15

動的型検査の弱点

プログラムを動かしてみないとわからない

  • $a = 13; $b = [];

// 環境依存の分岐 (本番環境じゃないと動かない)

if (is_production()) {

echo $a + $b; // 検出できない

echo 1 + []; // ←これはPHP7で検出できるようになった

}

function f(): int {

return []; // これも検出できない

}

Page 16

PHPDoc

#とは何か

Page 17

DocCommentとリフレクション

定義文 に付属する
(クラス・関数・メソッドなど)

特別なコメント実行時に

/** */
( 範囲)

文字列として取得することができる(DocComment)

/** ここはDocCommentだよ */

function hoge() {

}

$ref = new \ReflectionFunction('hoge');

echo $ref->getDocComment();

// => "/** ここはDocCommentだよ */"

Page 18

PHPDoc型注釈

DocCommentに型定義を注釈として記述する記法。
PhpStormPhanが解釈してくれる。

/**

* @param float $h

* @param float $a

* @param float $b

* @return float

*/

function trapezoid($h, $a, $b) {

return $h * ($a + $b) / 2;

}

Page 19

PhpStormの型表示

Page 20

PHPDocの型

基本はPHPの型かクラス名を書く

  • callable $this self static void

, , , , なども有効

  • 複合型 (Union/Multiple types)
  • int int|string
    「 または文字列」
    • 値が並んでるもの (配列/Collection)
  • int int[]
    「 が並んでるもの」
    • https://zonuexe.github.io/phpDocumentor2-ja/references/phpdoc/types.html

Page 21

なぜPHPDoc型注釈?

いきなり実装コードに型定義を追加すると、

  • いままで顕在化しなかったバグが発生するおそれ
    不用意なキャストや型検査でエラー発生

PHPDocはただのコメントなので、

  • 稼動中のコードの挙動を変更せず検査できる
    現行のPHPの型定義以上の型付けを表現できる
  • array int[]
    複合型やコレクション( → )
    • string int
      PHP5で対応できない , など

Page 22

これだけ覚えて帰ってね

タグ名 意味 例

@param 引数を定義

@param int $n1

@return 返り値を定義

@return int[]

@var 変数/プロパティを定義

@var int

@property マジックプロパティを定義

@property int $id

@property

ふつうのプロパティは じゃなくて

@var

なので注意

@property
(Phanは 未対応)

Page 23

@var

プロパティの例 ( )

@var

ふつうのプロパティの場合は を使って書く

final class Book {

/** @var string */

public $title;

/** @var \MyApp\Author[] */

public $authors;

/** @var \MyApp\ISBN */

public $isbn;

}

Page 24

@property

プロパティの例 ( )

拙作のzonuexe/objectsystemを使った場合

/**

* @property string $title

* @property \MyApp\Author[] $authors

* @property \MyApp\ISBN $isbn

*/

final class Book {

use \Teto\Object\TypedProperty;

protected static $property_types = [

'title' => 'string',

'authors' => 'MyApp\Author[]',

'isbn' => '?MyApp\ISBN',

];

}

Page 25

型注釈を使ったハック

複合型とコレクション(型の配列)を組み合せる

/** @return Book[]|\ArrayObject */

function getBooks() {

$data = hogehoge();

return new \ArrayObject($data);

}

$books = getBooks();

$books->▍ // ここで \ArrayObject の補完が効く

foreach ($books as $book) {

$book->▍ // ここで Book の補完が効く

}

Page 26

Phanの紹介

Page 27

Phanとは何か

ハンドメイドマーケットを運営する

アメリカのEtsy社が開発する静的解析ツール

https://github.com/etsy/phan

現在PHP作者のRasmus Lerdorf が所属し、

  • Phanの開発にも参加してる
    PHPの型定義と型注釈を両方見てくれる
  • @property @method
    ただし、 と 未対応

Page 28

Phanの導入

https://github.com/etsy/phan/releases

  • .phar
    最新のタグが付いてる ファイルをダウンロード
    PHP7とphp-astが必須

ただしラッパースクリプトを用意すれば、

  • サービスがPHP7で稼動してなくても動かせる
    #!/bin/sh

/path/to/php7/bin/php ${PHAN_BIN:-/path/to/bin/phan} "$@"

Page 29

Phanの設定

はじめにWikiから設定ファイルをコピペして、

.phan/config.php

プロジェクトの に保存

https://github.com/etsy/phan/wiki/Getting-Started#creating-a-config-file

// ディレクトリホワイトリスト

'directory_list' => [

'src',

'vendor/symfony/console',

],

// ディレクトリブラックリスト

"exclude_analysis_directory_list" => [

'vendor/'

],

Page 30

Phanの設定(そのほか)

  • 'generic_types_enabled' => bool
    @template

タグ(ジェネリック) 有効/無効

  • 'suppress_issue_types' => string[]
    無視するIssueタイプを指定する
  • 'plugins' => string[]
    Phanプラグインを追加できる
    • DollarDollar
      サンプルとして が添付されている
    • $$var

(可変変数)を検出

Page 31

Phan コマンド

-o, --output <filename>

出力ファイル指定

  • -m <mode>, --output-mode

出力形式

  • -x, --dead-code-detection

デッドコード検出

  • --backward-compatibility-checks

PHP7互換性

  • -j, --processes <int>

並列実行数

  • -p, --progress-bar

進捗バーを表示

./phan-wrapper -m csv -o phan.csv -x -j4 \

--progress-bar --backward-compatibility-checks

Page 32

Phan 出力

デフォルトの場合の出力

pixiv-lib/Illust/Common.php:738 PhanTypeMismatchArgumentInternal

Argument 1 (month) is string but \checkdate() takes int

pixiv-lib/Illust/Common.php:738 PhanTypeMismatchArgumentInternal
Argument 2 (day) is string but \checkdate() takes int

path/to/file.php:333 PhanTypeMismatchArgumentInternal This is message.

Issueと呼ばれる

PhanTypeMismatchArgumentInternal

  • @suppress
    Issueごとに出力するを制御可能( タグ)

Page 33

Phan Issueいろいろ

PhanParamTooFew PhanParamTooMany

/

  • 引数が足りない/多い
    • PhanParamTypeMismatch
  • 引数の型が間違ってる
    • PhanRedefineClass PhanRedefineFunction

/

  • 同名のクラス/関数がいくつも定義されてる

Page 34

PhanUndeclaredClassCatch

存在しないクラスでキャッチしようとしてないか

// PHP 5

namespace MyApp;

try {

foo();

} catch (RuntimeException $e) {

// ↑ \MyApp\RuntimeException

} catch (\Excaption $e) {

// ↑ \Exception が正しい

}

Page 35

PhanUndeclaredClassInstanceof

instanceof

存在しないクラスで してないか

namespace MyApp;

$e = get_error();

if ($e instanceof RuntimeException) {

// ↑ \MyApp\RuntimeException

} elseif ($e instanceof \Excaption) {

// ↑ \Exception が正しい

}

Page 36

PhanTypeMismatchForeach

foreachできない値をforeachしようとしてる

$iter = null;

// E_WARNING

foreach ($iter as $i) {

echo $i;

}

Page 37

PhanTypeMismatchReturn

定義と違った型の値を返してる

/** @return void */

function f() {

return null;

}

function g():int {

return [];

}

Page 38

PhanTypeMissingReturn

特定の型を返すべきだが、何も返していない

/** @return int */

function g(){

return;

}

Page 39

PhanTypeVoidAssignment

null

実際には が代入されるが

void

は「データがない」を意味するので適切ではな

/** @return void */

function g(){ return; }

$result = g();

Page 40

PhanNoopXXXX

無意味なコードを検出する

// 代入しない配列 PhanNoopArray

[1, 2, 3];

// 代入も実行もしないクロージャ PhanNoopClosure

function(){ return awesome(); };

// returnしていないので無意味 PhanNoopProperty

class C {

public $p;

function f() { $this->p; }

}

Page 41

PhanDeprecatedFunction

廃止予定/非推奨のメソッドを利用。

古い実装を新しいものに置換する際に

@deprecated

を付けておくと洗い出せてべんり

function newAwesomeFunc(){}

/** @deprecated */

function oldFunc(){}

oldFunc(); // PhpStormは取り消し線で表示

Page 42

Phan独自のアノテーション

@suppress <issue_type>
クラス・メソッドなどの範囲でIssueを無効化する

/** @suppress PhanUndeclaredConstant */

class C {

function f() { return AWESOME_CONSTANT; }

/** @supress PhanUndeclaredProperty */

function g() { return $this->p; }

}

Page 43

Phan独自のアノテーション

@template

型変数(ジェネリック)

/** @template T1 */

class Container {

/** @var T1 */

private $value;

public function __construct($value){ $this->value =
$value; }

function getValue() { return $value; }

}

Page 44

Phanの制限

define()

関数での定数定義はサポートされない

  • const
    動的に定義されるため ( は静的)
    • Reference to undeclared constant

扱い

  • @method

には対応してない

  • __call()

を使ったProxyパターン殺し

Page 45

Phanのめんどくさいところ

重い

  • ファイル数にもよるが数十秒〜 かかる
    • 標準化されてないPHPDocに惑わされる
  • PhanTypeMismatchReturn Returning type bool
    • but insert() is declared to return \成功
      「@return 成功 true 失敗 false」

Page 46

Phanの注意点

メソッド/プロパティの動的定義に未対応

  • __get() __set() __call() __callStatic()

, , , など

PHP用語としての「オーバーロード」

http://php.net/manual/ja/language.oop5.overloading.php

定数の動的定義にも未対応

  • define() const

はだめ。 文ならOK。

Page 47

Phanの比較対象(競合)

定番:PHP Mess Detector (PHPMD)

  • さまざまな指摘をしてくれるが、型検査はなし
    • 似たサービスもいくつかある (private有料)
  • Scrutinizer, SensioLabsInsight, CodeClimate
    • これらコードの怪しい兆候を細かに教えてくれる
    • 型検査の精度ではPhanの方が良く見える

Page 48

まとめ

PHPDocの型注釈は動作に影響を及ぼさずに

  • 型を宣言することができる
    Phanはソースコードを動作させずに解析できる

Phanで静的解析することで、

  • リファクタリングのリスクを減らすことができる

廃止予定の非推奨メソッドには

  • @deprecated

をつける習慣をつけると殲滅が捗る