PHP: PHPの静的解析(phpcs, phpmd, etc...)

名前 コマンド 概要 備考
Syntax check(lint) php -l 構文チェック php.exe をコマンドラインから実行する
phan phan 品質チェック PHP: Phan (PHP静的解析ツール)
PHP Static Analysis phpsa 品質チェック Smart Analysis for PHP
PHPStan phpstan 品質チェック 動作にPHP7以上が必要
php-nag phpnag 品質チェック 非推奨関数、弱い比較(== !=)、fall through などの検知
PHP Mess Detector phpmd 品質チェック
phpmd index.php text [rule]
[rule] cleancode,codesize,controversial,design,naming,unusedcode
Copy/Paste Detector phpcpd コピペ検知 重複コード 検知。
Dead Code Detector phpdcd デッドコード検知 到達不能コード 検知。
PHP_CodeSniffer
phpcs
phpcbf
コーディング規約
phpcs (Checker), phpcbf (Fixer)
PHPCompatibility との組み合わせで、互換性のチェックも可能。
他の 修正ツール としては PHP Coding Standards Fixer もある。
twig-lint twig-lint 構文チェック テンプレートエンジン Twig の linter

注釈

PHPバージョンの互換性チェックツールについて

参考

PHP マニュアル の付録 の移行ガイド

チェックツール(PHPCompatibility 以外)

  • PHPCodeFixer - 非推奨の関数、変数、およびiniディレクティブ
  • PHP Migration - PHPバージョンの移行と互換性チェック
  • php7cc - PHP7 互換性 チェッカー
  • php7mar - PHP7移行アシスタントレポート

修正ツール

メトリクス

複数のQAツールを実行するツール

その他

注釈

SonarSource から提供されているツールもPHPに対応している。(Coding Rules(PHP), SonarAnalyzer for PHP)


構文チェック(Syntax check (lint))

コマンドライン(php -l) でチェックする。 大量のファイルチェックで、環境に依存したくない場合、 PHP Parallel LintPHPLint も検討。

C:\>php -l index.php
No syntax errors detected in index.php

C:\>

注釈

カレントディレクトリー配下全てのファイルを対象としたい場合のスクリプト

Windows の場合
@for /r . %%f in (*.php,*.inc,*.html) do php -l "%%f"
Windows の場合(並列)
@for /r . %%f in (*.php,*.inc,*.html) do start php -l "%%f"
Windows の場合(相対パス表示)
@echo off
setlocal EnableDelayedExpansion
for /r . %%F in (*.php,*.inc,*.html) do (
    for /f "delims=" %%L in ('php -l "%%F"') do (
        set line=%%L
        echo !line:%~dp0=!
    )
)
endlocal
Linux の場合
$ find ./ -name "*.php" | xargs -n1 php -l

コーディング規約のチェック(PHP_CodeSniffer)

PHP_CodeSniffer でチェックする。

注釈

コーディング規約の 修正ツール PHP Coding Standards Fixer のPharファイルをダウンロードして修正する場合

修正実行

php php-cs-fixer.phar fix /path/to/project --level=psr2

アップデート

php php-cs-fixer.phar self-update

準備: PHP_CodeSniffer のインストール

composer global require "squizlabs/php_codesniffer=*"

とかでインストールして、パスの通ったところに、以下の内容で、 phpcs.cmd ファイルを用意する。

@%USERPROFILE%\AppData\Roaming\Composer\vendor\bin\phpcs %*

実行

C:\>phpcs index.php

FILE: C:\index.php
----------------------------------------------------------------------
FOUND 7 ERRORS AND 6 WARNINGS AFFECTING 11 LINES
----------------------------------------------------------------------
  1 | ERROR   | [x] End of line character is invalid; expected "\n"
    |         |     but found "\r\n"
  2 | ERROR   | [ ] Missing file doc comment
  2 | ERROR   | [ ] Missing function doc comment
  2 | ERROR   | [x] Opening brace should be on a new line
 16 | ERROR   | [x] Line indented incorrectly; expected 12 spaces,
    |         |     found 16
 24 | WARNING | [ ] Line exceeds 85 characters; contains 126
    |         |     characters
 57 | ERROR   | [x] Multi-line function call not indented correctly;
    |         |     expected 16 spaces but found 12
 58 | ERROR   | [x] Closing parenthesis of a multi-line function call
    |         |     must be on a line by itself
----------------------------------------------------------------------
PHPCBF CAN FIX THE 5 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Time: 38ms; Memory: 4.25Mb


C:\>

PHPの互換性のチェック(PHPCompatibility)

PHPのバージョンによって非推奨になるものなどを PHPCompatibility でチェックする。
(PHP_CodeSniffercomposer のグローバルにインストール済みであること)

準備: PHPCompatibility のインストール

cd %USERPROFILE%\AppData\Roaming\Composer\vendor\squizlabs\php_codesniffer\CodeSniffer\Standards
clone git clone git://github.com/wimg/PHPCompat_CodeSniffer.git PHPCompatibility

実行

C:\>phpcs --standard=PHPCompatibility index.php

FILE: C:\index.php
----------------------------------------------------------------------
FOUND 2 ERRORS AND 3 WARNINGS AFFECTING 5 LINES
----------------------------------------------------------------------
  1 | ERROR   | Default timezone is required since PHP 5.4
 11 | WARNING | The use of function dl is discouraged from PHP
    |         | version 5.3
 16 | ERROR   | Using a call-time pass-by-reference is prohibited
    |         | since php 5.4
 19 | WARNING | The use of long predefined variables has been
    |         | deprecated in 5.3 and removed in 5.4; Found
    |         | '$HTTP_POST_VARS'
 22 | WARNING | The use of function split is discouraged from PHP
    |         | version 5.3; use preg_split instead
----------------------------------------------------------------------

Time: 15ms; Memory: 3Mb


C:\>

SonarLint for Command Line

SonarLint for Command Line (要Javaランタイム)では、コマンドラインから、HTMLレポートが作成可能。

注釈

SonarQube (SonarLint) も参照。
C:\works\project>sonarlint --help
C:\tools\sonarlint-cli-2.0\bin\..
INFO:
INFO: usage: sonarlint [options]
INFO:
INFO: Options:
INFO:  -u,--update              Update binding with SonarQube server before analysis
INFO:  -D,--define <arg>        Define property
INFO:  -e,--errors              Produce execution error messages
INFO:  -h,--help                Display help information
INFO:  -v,--version             Display version information
INFO:  -X,--debug               Produce execution debug output
INFO:  -i,--interactive         Run interactively
INFO:  --html-report <path>     HTML report output path (relative or absolute)
INFO:  --src <glob pattern>     GLOB pattern to identify source files
INFO:  --tests <glob pattern>   GLOB pattern to identify test files
INFO:  --exclude <glob pattern> GLOB pattern to exclude files
INFO:  --charset <name>         Character encoding of the source files

プロジェクトルートで実行すると、 .sonarlint\sonarlint-report.html にHTMLレポート(Issues per Rule の下のリストボックスからフィルター可能)を生成する。

C:\works\project>sonarlint --src **/src/**
C:\tools\sonarlint-cli-2.0\bin\..
INFO: Java 1.8.0_111 Oracle Corporation (32-bit)
INFO: Windows 10 10.0 x86
INFO: Standalone mode
INFO: Index files
INFO: 5 files indexed
INFO: 5 source files to be analyzed
INFO: 5/5 source files have been analyzed
INFO:

-------------  SonarLint Report  -------------

          81 issues (5 files analyzed)

          10 critical
          37 major
          14 minor
          20 info

-------------------------------------------


INFO: SonarLint HTML Report generated: C:\works\project\.sonarlint\sonarlint-report.html
INFO: ------------------------------------------------------------------------
INFO: EXECUTION SUCCESS
INFO: ------------------------------------------------------------------------
INFO: Total time: 4.802s
INFO: Final Memory: 3M/27M
INFO: ------------------------------------------------------------------------

C:\works\project>start .sonarlint\sonarlint-report.html

自力で構文解析する。

token_get_all 関数で、PHPでPHPのソースコードの解析ができる。

注釈

パーサーライブラリーとして、 PHP Parser があり、専用のPHP7以上の実行環境が用意できる場合、拡張モジュール php-ast も検討候補。 また、NodeJS ライブラリーとしては、 php-parser がある。

準備: static_analysis.filter.cli.php

$_GET $_POST とかに filter_input 使えと指摘したい。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php
function analyze($filepath) {
    $contents = file_get_contents($filepath);
    if ($contents === false) {
        echo "File open error at {$filepath}".PHP_EOL;
    } else {
        foreach (token_get_all($contents) as $token) {
            // see: http://php.net/manual/tokens.php
            if ($token[0] === T_VARIABLE) {
                if (in_array($token[1], array('$_SERVER', '$_GET', '$_POST', '$_ENV', '$_COOKIE'))) {
                    $message = 'use filter_input(INPUT'.substr($token[1], 1).', $name) or filter_input_array() instead';
                    echo "Detect: {$filepath}({$token[2]}): {$token[1]} => {$message}".PHP_EOL;
                }
            }
        }
    }
}
for ($i = 1; $i < $argc; ++$i) {
    if (is_dir($argv[$i])) {
        foreach (
            new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($argv[$i]),
                RecursiveIteratorIterator::CHILD_FIRST
            ) as $iterator
        ) {
            if (!$iterator->isDir()) {
                //echo 'File: '.$iterator->getPathname().PHP_EOL;
                analyze($iterator->getPathname());
            }
        }
    } else {
        //echo 'File: '.$argv[$i].PHP_EOL;
        analyze($argv[$i]);
    }
}

準備: static_analysis.session_unset.cli.php

  • ASCII 32以下の全ての文字。\t (0x09), \n (0x0a) , \r (0x0d) は除く
  • session_unset (session_unset の注意参照)
  • unset($_SESSION); (session_unset の警告参照)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
function analyze($filepath) {
    $contents = file_get_contents($filepath);
    if ($contents === false) {
        echo "File open error at {$filepath}".PHP_EOL;
    } else {
        $is_unset = false;
        foreach (token_get_all($contents) as $token) {
            // see: http://php.net/manual/tokens.php
            switch ($token[0]) {
                case T_BAD_CHARACTER:
                    echo "Detect: {$filepath}({$token[2]}): T_BAD_CHARACTER".PHP_EOL;
                    break;
                case T_STRING:
                    if ($token[1] === 'session_unset') {
                        echo "Detect: {$filepath}({$token[2]}): {$token[1]}() => unset(\$_SESSION['varname']);".PHP_EOL;
                    }
                    break;
                case T_UNSET:
                    $is_unset = true;
                    break;
                case T_VARIABLE:
                    if ($is_unset === true && $token[1] === '$_SESSION') {
                        echo "Error: {$filepath}({$token[2]}): 'unset(\$_SESSION);' see: http://php.net/manual/function.session-unset.php".PHP_EOL;
                    }
                    break;
                case ';':
                    $is_unset = false;
                    break;
                default:
                    break;
            }
        }
    }
}
for ($i = 1; $i < $argc; ++$i) {
    if (is_dir($argv[$i])) {
        foreach (
            new RecursiveIteratorIterator(
                new RecursiveDirectoryIterator($argv[$i]),
                RecursiveIteratorIterator::CHILD_FIRST
            ) as $iterator
        ) {
            if (!$iterator->isDir()) {
                //echo 'File: '.$iterator->getPathname().PHP_EOL;
                analyze($iterator->getPathname());
            }
        }
    } else {
        //echo 'File: '.$argv[$i].PHP_EOL;
        analyze($argv[$i]);
    }
}

実行

C:\>php static_analysis.filter.cli.php index.php
Detect: index.php(20): $_GET => use filter_input(INPUT_GET, $name) or filter_input_array() instead

C:\>php static_analysis.session_unset.cli.php index.php
Error: index.php(17): 'unset($_SESSION);' see: http://php.net/manual/function.session-unset.php
Detect: index.php(18): session_unset() => unset($_SESSION['varname']);

C:\>