背景分析
不知道你的專案是否有遇到過類似的線上故障呢?比如
繼承類語法錯誤導致的故障
檔案1
class Animal { public $hasLeg = false; }
檔案2
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
(注意 IDE 並沒有提示有預發錯誤的喲,我專門截圖)
今天在看程式碼的時候看到一個變數一直重複查詢,就是使用者是否是管理員的身份。我想既然這樣,不然在第一次用的地方就放入到成員變數裡,免得後面都重複查詢。
結果發現我在父類別定義的變數名$isAdmin,之前的程式碼已經在某一個子類裡面單獨定義過了。父類別裡是public屬性,而子類裡是private導致了這個故障。
如果是 java 這種錯誤,無法編譯通過。但是 php 不需要編譯,只要測試沒有覆蓋到剛剛修改的檔案就不會發現這個問題,既是優勢也是弱勢。
引數不符合預期
有時候a.php,b.php,c.php三個檔案都參照d.php的的一個函數,但是修改了d.php裡面的一個函數的引數個數,如果前面使用的3個檔案裡面的沒有改全,只改了a.php,而測試的時候又沒有覆蓋到b.php和c.php,那麼上線了,就會觸發bug和錯誤了。
錯把陣列當物件
你可能認為這種錯誤太低階了,不可能發生在自己身上,但是根據我的經驗的確會發生,高強度的需求之下,很容易複製貼上一些東西,只複製一半。而且恰巧因為某些邏輯判斷,自己在日常環境開發的時候,出現問題的地方沒有被執行到。
比如下面這段程式碼:
$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)的判斷導致沒有執行到裡面去。直到上線之後才發現問題。
錯把物件當陣列
Cannot use object of type DataObjectArticle as array
不禁反思,如果這個專案是 java 的,肯定不會出現上面兩個問題了,因為在專案構建的時候就已經沒法通過了。
不存在的陣列
這也不飄紅?多寫了個s呢,可能因為外面包了一個empty所以IDE沒有標記為錯誤吧。所以我們不能太相信IDE。
思考與改進
自造輪子實驗
進一步思考,我們是否能夠做一個工具來自己模擬編譯呢?寫了一個小 demo ,依賴nikic/php-parser
https://github.com/nikic/PHP-Parser
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 PhpParserError; use PhpParserNodeStmtProperty; use PhpParserParserFactory; use PhpParserNodeStmtClass_; $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 "33[01;35m[YUNQI] 33[01;31m" echo [email protected] echo -e -n "33[00m" } Color_Text() { echo -e " e[0;$2m$1e[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 runn" Echo_Yellow "========================n" ./vendor/bin/phan -f file.list -o res.out Echo_Yellow "========================n" Echo_Red "error logn" Echo_Red "========================n" cat res.out Echo_Red "========================n"
執行結果
案例中的錯誤
1.類不存在
2.引數型別錯誤
3.語法運算型別推斷
實驗2
新增一個src/b.php
<?php class B{ }
執行結果
能過自動查詢到class B了,不用我們做自動載入規則的指定
實驗3
剛剛兩個都是測試的單獨的指令碼,沒有測試專案,其實Plan已經支援了。假如我有一個專案如下
我在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
如圖所示呢,說明根據專案的自動載入規則A,B,C三個類呢都被掃描到了。
以上就是【Phan】程式碼靜態掃描的詳細內容,更多請關注TW511.COM其它相關文章!