使用 Phan 爲你的 PHP 項目保駕護航 - 代碼靜態掃描

原文:個人我的博客 https://mengkang.net/1356.html
工做了兩三年,技術停滯不前,迷茫沒有方向,不如看下個人直播 PHP 進階之路 (金三銀四跳槽必考,通常人我不告訴他)

不少時候,最大的優點在某些狀況下就會變成最大的劣勢。PHP 語法很是靈活,也不用編譯。可是在項目比較複雜的時候,可能會致使一些意想不到的 bug。php

背景分析

不知道你的項目是否有遇到過相似的線上故障呢?好比html

繼承類語法錯誤致使的故障

文件1java

class Animal
{
    public $hasLeg = false;
}

文件2node

include "Animal.php";

class Dog extends Animal
{
    protected $hasLeg = false;
}

$dog = new Dog();
php Dog.php

Fatal error: Access level to Dog::$hasLeg must be public (as in class Animal) in /Users/mengkang/vagrant-develop/project/untitled1/Dog.php on line 5

image.png
(注意 IDE 並無提示有預發錯誤的喲,我專門截圖)git

今天在看代碼的時候看到一個變量一直重複查詢,就是用戶是不是管理員的身份。我想既然這樣,否則在第一次用的地方就放入到成員變量裏,省得後面都重複查詢。github

結果發現我在父類定義的變量名$isAdmin,以前的代碼已經在某一個子類裏面單獨定義過了。父類裏是public屬性,而子類裏是private致使了這個故障。shell

若是是 java 這種錯誤,沒法編譯經過。可是 php 不須要編譯,只要測試沒有覆蓋到剛剛修改的文件就不會發現這個問題,既是優點也是弱勢。json

參數不符合預期

image.png

有時候a.php,b.php,c.php三個文件都引用d.php的的一個函數,可是修改了d.php裏面的一個函數的參數個數,若是前面使用的3個文件裏面的沒有改全,只改了a.php,而測試的時候又沒有覆蓋到b.phpc.php,那麼上線了,就會觸發bug和錯誤了。vim

錯把數組當對象

你可能認爲這種錯誤過低級了,不可能發生在本身身上,可是根據個人經驗的確會發生,高強度的需求之下,很容易複製粘貼一些東西,只複製一半。並且恰巧由於某些邏輯判斷,本身在平常環境開發的時候,出現問題的地方沒有被執行到。
好比下面這段代碼:segmentfault

$article = $this->getParam('article');

// 假設下面這段代碼是複製的
$isPowerEditer = "xxxxx 演示代碼";

if(!$isPowerEditer){
    if ($article->getUserId() != $uid)
    {
        ...
    }
}

由於複製的來源處,$article是一個對象,因此調用了getUserId的方法。可是上面的$article是一個從客戶端獲取的參數,不是對象。

Call to a member function getUserId() on a non-object

而本身測試的時候,由於if(!$isPowerEditer)的判斷致使沒有執行到裏面去。直到上線以後才發現問題。

錯把對象當數組

image.png

Cannot use object of type DataObject\Article as array

不由反思,若是這個項目是 java 的,確定不會出現上面兩個問題了,由於在項目構建的時候就已經無法經過了。

不存在的數組

image.png
這也不飄紅?多寫了個s呢,可能由於外面包了一個empty因此IDE沒有標記爲錯誤吧。因此咱們不能太相信IDE。

思考與改進

自造輪子實驗

進一步思考,咱們是否可以作一個工具來本身模擬編譯呢?寫了一個小 demo ,依賴nikic/php-parser

https://github.com/nikic/PHP-...

PHP-Parser 能夠把PHP代碼解析爲AST,方便咱們作語法分析。好比上面的例子
文件1

class Animal
{
    public $hasLeg = false;
}

文件2(Dog.php)

include "Animal.php";

class Dog extends Animal
{
    protected $hasLeg = false;
}

$dog = new Dog();

咱們利用 PHP-Parser 作了語法解析檢測,代碼以下:

include dirname(__DIR__)."/vendor/autoload.php";

use PhpParser\Error;
use PhpParser\Node\Stmt\Property;
use PhpParser\ParserFactory;
use PhpParser\Node\Stmt\Class_;

$code = file_get_contents("Dog.php");

$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP5);

try {
    $ast = $parser->parse($code);
} catch (Error $error) {
    echo "Parse error: {$error->getMessage()}\n";
    return;
}

$classCheck = new ClassCheck($ast);
$classCheck->extendsCheck();


class ClassCheck{

    /**
     * @var Class_[]|null
     */
    private $classTable;

    public function __construct($nodes)
    {
        foreach ($nodes as $node){
            if ($node instanceof Class_){
                $name = $node->name;
                if (!isset($this->classTable[$name])) {
                    $this->classTable[$name] = $node;
                }else{
                    // 報錯哪裏類重複了
                    echo $node->getLine();
                }
            }
        }
    }

    public function extendsCheck(){

        foreach ($this->classTable as $node){
            if (!$node->extends){
                continue;
            }

            $parentClassName = $node->extends->getFirst();

            if (!isset($this->classTable[$parentClassName])) {
                exit($parentClassName."不存在");
            }

            $parentNode = $this->classTable[$parentClassName];

            foreach ($node->stmts as $stmt){
                if ($stmt instanceof Property){
                    // 查看該屬性是否存在於父類中
                    $this->propertyCheck($stmt,$parentNode);
                }
            }
        }
    }

    /**
     * @param Property $property
     * @param Class_ $parentNode
     */
    private function propertyCheck($property,$parentNode){
        foreach ($parentNode->stmts as $stmt){
            if ($stmt instanceof Property){
                if ($stmt->props[0]->name != $property->props[0]->name){
                    continue;
                }

                if ($stmt->isProtected() && $property->isPrivate()) {
                    echo $stmt->getLine()."\n";
                    echo $property->getLine()."\n";
                }
            }
        }
    }
}

原理能就是對解析出來的AST繼續作分析,可是前人栽樹後人乘涼,這樣的完整工具已經有大神幫咱們作好了。

使用現有工具

https://github.com/phan/phan

能夠說它與上面介紹的nikic/php-parser師出同門,依賴nikic/php-astPHP擴展

先安裝php-ast擴展

大概描述安裝步驟

git clone https://github.com/nikic/php-ast
cd php-ast/
phpize
sudo ./configure --enable-ast
sudo make
sudo make install
cd /etc/php.d
# 引入擴展
sudo vim ast.ini
# 就能看到擴展啦
php -m | grep ast

安裝 composer

大概描述安裝步驟

curl -sS https://getcomposer.org/installer | php

安裝plan

mkdir test
cd test
~/composer.phar require --dev "phan/phan:1.x"

實驗

實驗1

新建個項目,隨便寫個有問題的代碼

路徑是src/a.php

<?php

class A extends B
{
    public function a1()
    {
        return $this->a2(1);
    }

    /**
     * @param array $b
     *
     * @return int
     */
    private function a2($b)
    {
        return $b + 1;
    }
}

寫個shell腳本

#!/bin/bash

function log()
{
    echo -e -n "\033[01;35m[YUNQI] \033[01;31m"
    echo $@
    echo -e -n "\033[00m"
}

Color_Text()
{
  echo -e " \e[0;$2m$1\e[0m"
}

Echo_Red()
{
  echo $(Color_Text "$1" "31")
}

Echo_Green()
{
  echo $(Color_Text "$1" "32")
}

Echo_Yellow()
{
  echo $(Color_Text "$1" "33")
}

: > file.list

for file in $(ls src/*)
do
  echo $file >> file.list
done

Echo_Green "file list:\n"
Echo_Green "========================\n"

cat file.list

Echo_Green "========================\n"


Echo_Yellow "Phan run\n"
Echo_Yellow "========================\n"

./vendor/bin/phan -f file.list -o res.out

Echo_Yellow "========================\n"

Echo_Red "error log\n"
Echo_Red "========================\n"

cat res.out

Echo_Red "========================\n"
執行結果

案例中的錯誤

  1. 類不存在
  2. 參數類型錯誤
  3. 語法運算類型推斷

image.png

實驗2

新增一個src/b.php

<?php
class B{

}
執行結果

能過自動查找到class B了,不用咱們作自動加載規則的指定

image.png

實驗3

剛剛兩個都是測試的單獨的腳本,沒有測試項目,其實Plan已經支持了。假如我有一個項目以下
image.png

我在composer.json裏面指定自動加載規則

{
  "require-dev": {
    "phan/phan": "1.x"
  },
  "autoload": {
    "psr-4": {
      "Mk\\": "src"
    }
  }
}

而後在項目根目錄執行

./vendor/bin/phan --init --init-level=3

而後就會生成默認的配置文件在.phan目錄裏,最後就能夠執行靜態檢測命令了

./vendor/bin/phan --progress-bar

image.png

如圖所示呢,說明根據項目的自動加載規則A,B,C三個類呢都被掃描到了。

看到這裏,是否是有想把本身項目上線流程裏面加上靜態語法檢測呢?心動不如行動。

也歡迎你們關注個人公衆號,不發騷擾,只發乾貨原創文章
圖片描述

相關文章
相關標籤/搜索