摘要:比如上面的例子文件文件我們利用做了語(yǔ)法解析檢測(cè),代碼如下報(bào)錯(cuò)哪里類重復(fù)了不存在查看該屬性是否存在于父類中原理能就是對(duì)解析出來(lái)的繼續(xù)做分析,但是前人栽樹(shù)后人乘涼,這樣的完整工具已經(jīng)有大神幫我們做好了。
原文:我的個(gè)人博客 https://mengkang.net/1356.html
工作了兩三年,技術(shù)停滯不前,迷茫沒(méi)有方向,不如看下我的直播 PHP 進(jìn)階之路 (金三銀四跳槽必考,一般人我不告訴他)
很多時(shí)候,最大的優(yōu)勢(shì)在某些情況下就會(huì)變成最大的劣勢(shì)。PHP 語(yǔ)法非常靈活,也不用編譯。但是在項(xiàng)目比較復(fù)雜的時(shí)候,可能會(huì)導(dǎo)致一些意想不到的 bug。
背景分析不知道你的項(xiàng)目是否有遇到過(guò)類似的線上故障呢?比如
繼承類語(yǔ)法錯(cuò)誤導(dǎo)致的故障文件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 并沒(méi)有提示有預(yù)發(fā)錯(cuò)誤的喲,我專門截圖)
今天在看代碼的時(shí)候看到一個(gè)變量一直重復(fù)查詢,就是用戶是否是管理員的身份。我想既然這樣,不然在第一次用的地方就放入到成員變量里,免得后面都重復(fù)查詢。
結(jié)果發(fā)現(xiàn)我在父類定義的變量名$isAdmin,之前的代碼已經(jīng)在某一個(gè)子類里面多帶帶定義過(guò)了。父類里是public屬性,而子類里是private導(dǎo)致了這個(gè)故障。
如果是 java 這種錯(cuò)誤,無(wú)法編譯通過(guò)。但是 php 不需要編譯,只要測(cè)試沒(méi)有覆蓋到剛剛修改的文件就不會(huì)發(fā)現(xiàn)這個(gè)問(wèn)題,既是優(yōu)勢(shì)也是弱勢(shì)。
參數(shù)不符合預(yù)期有時(shí)候a.php,b.php,c.php三個(gè)文件都引用d.php的的一個(gè)函數(shù),但是修改了d.php里面的一個(gè)函數(shù)的參數(shù)個(gè)數(shù),如果前面使用的3個(gè)文件里面的沒(méi)有改全,只改了a.php,而測(cè)試的時(shí)候又沒(méi)有覆蓋到b.php和c.php,那么上線了,就會(huì)觸發(fā)bug和錯(cuò)誤了。
錯(cuò)把數(shù)組當(dāng)對(duì)象你可能認(rèn)為這種錯(cuò)誤太低級(jí)了,不可能發(fā)生在自己身上,但是根據(jù)我的經(jīng)驗(yàn)的確會(huì)發(fā)生,高強(qiáng)度的需求之下,很容易復(fù)制粘貼一些東西,只復(fù)制一半。而且恰巧因?yàn)槟承┻壿嬇袛?,自己在日常環(huán)境開(kāi)發(fā)的時(shí)候,出現(xiàn)問(wèn)題的地方?jīng)]有被執(zhí)行到。
比如下面這段代碼:
$article = $this->getParam("article"); // 假設(shè)下面這段代碼是復(fù)制的 $isPowerEditer = "xxxxx 演示代碼"; if(!$isPowerEditer){ if ($article->getUserId() != $uid) { ... } }
因?yàn)閺?fù)制的來(lái)源處,$article是一個(gè)對(duì)象,所以調(diào)用了getUserId的方法。但是上面的$article是一個(gè)從客戶端獲取的參數(shù),不是對(duì)象。
Call to a member function getUserId() on a non-object
而自己測(cè)試的時(shí)候,因?yàn)?b>if(!$isPowerEditer)的判斷導(dǎo)致沒(méi)有執(zhí)行到里面去。直到上線之后才發(fā)現(xiàn)問(wèn)題。
錯(cuò)把對(duì)象當(dāng)數(shù)組Cannot use object of type DataObjectArticle as array
不禁反思,如果這個(gè)項(xiàng)目是 java 的,肯定不會(huì)出現(xiàn)上面兩個(gè)問(wèn)題了,因?yàn)樵陧?xiàng)目構(gòu)建的時(shí)候就已經(jīng)沒(méi)法通過(guò)了。
不存在的數(shù)組
這也不飄紅?多寫了個(gè)s呢,可能因?yàn)橥饷姘艘粋€(gè)empty所以IDE沒(méi)有標(biāo)記為錯(cuò)誤吧。所以我們不能太相信IDE。
進(jìn)一步思考,我們是否能夠做一個(gè)工具來(lái)自己模擬編譯呢?寫了一個(gè)小 demo ,依賴nikic/php-parser
https://github.com/nikic/PHP-...
PHP-Parser 可以把PHP代碼解析為AST,方便我們做語(yǔ)法分析。比如上面的例子
文件1
class Animal { public $hasLeg = false; }
文件2(Dog.php)
include "Animal.php"; class Dog extends Animal { protected $hasLeg = false; } $dog = new Dog();
我們利用 PHP-Parser 做了語(yǔ)法解析檢測(cè),代碼如下:
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()} "; 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{ // 報(bào)錯(cuò)哪里類重復(fù)了 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()." "; echo $property->getLine()." "; } } } } }
原理能就是對(duì)解析出來(lái)的AST繼續(xù)做分析,但是前人栽樹(shù)后人乘涼,這樣的完整工具已經(jīng)有大神幫我們做好了。
使用現(xiàn)有工具https://github.com/phan/phan
可以說(shuō)它與上面介紹的nikic/php-parser師出同門,依賴nikic/php-astPHP擴(kuò)展
先安裝php-ast擴(kuò)展大概描述安裝步驟
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 # 引入擴(kuò)展 sudo vim ast.ini # 就能看到擴(kuò)展啦 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"實(shí)驗(yàn) 實(shí)驗(yàn)1
新建個(gè)項(xiàng)目,隨便寫個(gè)有問(wèn)題的代碼
路徑是src/a.php
a2(1); } /** * @param array $b * * @return int */ private function a2($b) { return $b + 1; } }
寫個(gè)shell腳本
#!/bin/bash function log() { echo -e -n "