#如何為PHPMD編寫規則
這篇文章教你怎樣給PHPMD寫規則類。這些類可以用來檢查被分析代碼中的設計問題或錯誤。
我們從PHPMD的一些基礎結構開始。PHPMD的所有規則一定是至少實現了[\PHPMD\Rule](https://github.com/phpmd/phpmd/blob/master/src/main/php/PHPMD/Rule.php)接口。也可以擴展抽象基類[\PHPMD\AbstractRule](https://github.com/phpmd/phpmd/blob/master/src/main/php/PHPMD/AbstractRule.php),這個類已經提供了所有必需的基礎設施的方法和應用邏輯的實現,所以你的唯一任務就是為規則編寫具體的驗證代碼。PHPMD規則接口聲明了apply()方法,在代碼分析階段會被應用調用到,從而執行驗證代碼。
```php
require_once 'PHPMD/AbstractRule.php';
class Com_Example_Rule_NoFunctions extends \PHPMD\AbstractRule
{
public function apply(\PHPMD\AbstractNode $node)
{
// Check constraints against the given node instance
}
}
```
apply()方法的參數是\PHPMD\AbstractNode實例。這個實例表達被分析代碼中發現的不同的高級別代碼構件。在這種情況下,高級別代碼構件意味著接口、類、方法和函數。我們當然不想在每一個規則中都重復執行決策代碼,但是要怎樣告訴PHPMD,這些代碼構件中,哪個才是我們規則感興趣的?為了解決這個問題,PHPMD使用所謂的標記接口。這些接口的唯一目的是給規則類打標簽。下面的列表顯示可用的標記接口:
- [\PHPMD\Rule\ClassAware](https://github.com/phpmd/phpmd/blob/master/src/main/php/PHPMD/Rule/ClassAware.php)
- [\PHPMD\Rule\FunctionAware](https://github.com/phpmd/phpmd/blob/master/src/main/php/PHPMD/Rule/FunctionAware.php)
- [\PHPMD\Rule\InterfaceAware](https://github.com/phpmd/phpmd/blob/master/src/main/php/PHPMD/Rule/InterfaceAware.php)
- [\PHPMD\Rule\MethodAware](https://github.com/phpmd/phpmd/blob/master/src/main/php/PHPMD/Rule/MethodAware.php)
有了這個標記接口,我們就可以擴展上一個例子,這樣,該規則將在分析被測代碼中發現函數時調用到。
```php
class Com_Example_Rule_NoFunctions
extends \PHPMD\AbstractRule
implements \PHPMD\Rule\FunctionAware
{
public function apply(\PHPMD\AbstractNode $node)
{
// Check constraints against the given node instance
}
}
```
假定我們的編碼準則禁止使用函數,每一次調用函數都會導致apply()方法產生一個規則違反信息。
PHPMD通過addViolation()方法將其報告出來。規則類從父類PHPMD\AbstractRule繼承這個助手方法。
```php
class Com_Example_Rule_NoFunctions // ...
{
public function apply(\PHPMD\AbstractNode $node)
{
$this->addViolation($node);
}
}
```
現在只需要在規則集中加入配置入口。規則集文件是一個XML文檔,可以配置一個或多個規則,可以配置全部規則設置,所以每個人都可以改造現有的規則,而無需改變其本身。本規則集文件的語法全部改變自PHPMD的示例。開始使用自定義規則集之前,你應該看看現有的XML文件,再調整新創建規則的配置。規則配置中最重要的元素是:
- @name: 易讀規則名。
- @message: 錯誤/違規信息。
- @class: 規則的全稱類名。
- priority: 規則優先權,值可以為1-5,1是最高優先權,5最低。
```xml
<ruleset name="example.com rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<rule name="FunctionRule"
message = "Please do not use functions."
class="Com_Example_Rule_NoFunctions"
externalInfoUrl="http://example.com/phpmd/rules.html#functionrule">
<priority>1</priority>
</rule>
</ruleset>
```
上面清單顯示了一個基本規則集文件,更多細節請參考[創建自定義規則集](http://phpmd.org/documentation/creating-a-ruleset.html)。
假設我們的Com/Example/Rule/NoFunction.php是一個規則類, example-rule.xml是一個規則集。可以從命令行中測試:
```bash
~ $ phpmd /my/source/example.com text /my/rules/example-rule.xml
/my/source/example.com/functions.php:2 Please do not use functions.
```
搞定。現在我們有了PHPMD的第一個自定義規則類。
## 根據現有的軟件度量編寫規則
開發PHPMD的根本目標是為PHP_Depend實現一個簡單友好的接口。本節你將會看到,如何開發一個規則類,并以輸入數據的形式使用PDepend作為軟件度量。
你會看到如何訪問一個給定的\PHPMD\AbstractNode實例軟件計量,以及如何使用phpmd配置后端,閾值等設置可定制而不改變任何PHP代碼。此外,您將看到怎樣改進錯誤信息的內容。
現在新規則需要一個軟件度量標準作基礎。完整并及時更新的可用軟件計量標準列表可以從PHP_Depend的計量類目獲得。在這里,我們選公共方法個數(npm:Number of Public Methods)標準,然后定義規則的上下限。我們設置上限10,如果存在更多的公共方法會暴露更多的類的內部信息,那么它應該被分成幾個類了。下限可以設為1,因為如果類沒有公有方法,那它就不會為周邊應用提供任何服務。
下面的代碼清單顯示整個規則類的骨架。你可以看到,它實現了\PHPMD\Rule\ClassAware接口,所以PHPMD知道這個規則只能被類調用。
```php
class Com_Example_Rule_NumberOfPublicMethods
extends \PHPMD\AbstractRule
implements \PHPMD\Rule\ClassAware
{
const MINIMUM = 1,
MAXIMUM = 10;
public function apply(\PHPMD\AbstractNode $node)
{
// Check constraints against the given node instance
}
}
```
接下來使用nmp規則對象,它和節點實例是關聯的,所有節點計算的規則對象都可以使用節點實例的getMetric()方法直接訪問。getMetric()接受一個參數,即規則的縮寫。這些縮寫可以從PHP_Depends規則分類中找到。
```php
class Com_Example_Rule_NumberOfPublicMethods
extends \PHPMD\AbstractRule
implements \PHPMD\Rule\ClassAware
{
const MINIMUM = 1,
MAXIMUM = 10;
public function apply(\PHPMD\AbstractNode $node)
{
$npm = $node->getMetric('npm');
if ($npm < self::MINIMUM || $npm > self::MAXIMUM) {
$this->addViolation($node);
}
}
}
```
以上就是規則的代碼部分。現在我們把它加到規則集文件中。
```xml
<ruleset name="example.com rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<!-- ... -->
<rule name="NumberOfPublics"
message = "The context class violates the NPM metric."
class="Com_Example_Rule_NumberOfPublicMethods"
externalInfoUrl="http://example.com/phpmd/rules.html#numberofpublics">
<priority>3</priority>
</rule>
</ruleset>
```
現在運行PHPMD,它會報告不滿足NPM規則的所有類。但正如我們所承諾的,我們將使這個規則更加可定制,以便可以調整它適應不同的項目要求。我們將MINIMUM和MAXIMUM替換為在規則集文件中可以配置的屬性。
```xml
<ruleset name="example.com rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<!-- ... -->
<rule name="NumberOfPublics"
message = "The context class violates the NPM metric."
class="Com_Example_Rule_NumberOfPublicMethods"
externalInfoUrl="http://example.com/phpmd/rules.html#numberofpublics">
<priority>3</priority>
<properties>
<property name="minimum"
value="1"
description="Minimum number of public methods." />
<property name="maximum"
value="10"
description="Maximum number of public methods." />
</properties>
</rule>
</ruleset>
```
PMD規則集文件中你可以隨便定義幾個屬性,隨你喜歡。它們會被PHPMD運行時環境注入到一個規則實例中,然后可以通過get<type>Property()方法訪問。當前PHPMD支持以下幾個getter方法。
getBooleanProperty()
getIntProperty()
(好像也只有兩個。。。上面的話真不要臉)
現在我們來修改規則類,用可配置屬性替換硬編碼常量。
```php
class Com_Example_Rule_NumberOfPublicMethods
extends \PHPMD\AbstractRule
implements \PHPMD\Rule\ClassAware
{
public function apply(\PHPMD\AbstractNode $node)
{
$npm = $node->getMetric('npm');
if ($npm < $this->getIntProperty('minimum') ||
$npm > $this->getIntProperty('maximum')
) {
$this->addViolation($node);
}
}
}
```
現在我們差不多完成了,還有一個小小細節。我們執行這個規則,用戶將收到消息“The context class violates the NPM metric.“這個消息不是真的有用,因為它必須手動檢查上下閾值與實際閾值。可以用PHPMD最簡模版/占位符引擎得到更詳細的信息,你可以定義(違規信息)占位符,這會被替換為真實值。占位符的格式是'{' + \d+ '}'。
```xml
<ruleset name="example.com rules"
xmlns="http://pmd.sf.net/ruleset/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://pmd.sf.net/ruleset/1.0.0 http://pmd.sf.net/ruleset_xml_schema.xsd"
xsi:noNamespaceSchemaLocation="http://pmd.sf.net/ruleset_xml_schema.xsd">
<!-- ... -->
<rule name="NumberOfPublics"
message = "The class {0} has {1} public method, the threshold is {2}."
class="Com_Example_Rule_NumberOfPublicMethods"
externalInfoUrl="http://example.com/phpmd/rules.html#numberofpublics">
<priority>3</priority>
<properties>
<property name="minimum"
value="1"
description="Minimum number of public methods." />
<property name="maximum"
value="10"
description="Maximum number of public methods." />
</properties>
</rule>
</ruleset>
```
現在我們可以以這種方式調整規則類,自動填充占位符 {0}, {1}, {2}
```php
class Com_Example_Rule_NumberOfPublicMethods
extends \PHPMD\AbstractRule
implements \PHPMD\Rule\ClassAware
{
public function apply(\PHPMD\AbstractNode $node)
{
$min = $this->getIntProperty('minimum');
$max = $this->getIntProperty('maximum');
$npm = $node->getMetric('npm');
if ($npm < $min) {
$this->addViolation($node, array(get_class($node), $npm, $min));
} else if ($npm > $max) {
$this->addViolation($node, array(get_class($node), $npm, $max));
}
}
}
```
如果我們運行這個版本的規則,我們會得到一個錯誤信息,如下圖所示。
```The class FooBar has 42 public method, the threshold is 10.```
## 編寫基于抽象語法樹的規則
現在我們將學習如何開發一個PHPMD規則,利用PHP_Depend的抽象語法樹檢測被分析源代碼的違規或可能的錯誤。訪問PHP_Depend的抽象語法樹給你PHPMD規則最強大的能力,你可以分析測試軟件幾乎所有的方面。語法樹可以通過\PHPMD\AbstractNode類的getFirstChildOfType()和findChildrenOfType()方法訪問。
在這個例子中我們將實現一個規則,用來檢測新但有爭議的PHP新功能,goto。因為我們都知道并同意Basic中的goto非常糟糕,我們想阻止我們的開發者使用壞的特征。因此,我們實現了一個PHPMD規則,通過PHP_Depend搜索goto語言構造。
goto語句不會出現在類和對象中,但會在方法和函數里出現。新規則類必需實現兩個標記接口,\PHPMD\Rule\FunctionAware 和 \PHPMD\Rule\MethodAware。
```php
namespace PHPMD\Rule\Design;
use PHPMD\AbstractNode;
use PHPMD\AbstractRule;
use PHPMD\Rule\MethodAware;
use PHPMD\Rule\FunctionAware;
class GotoStatement extends AbstractRule implements MethodAware, FunctionAware
{
public function apply(AbstractNode $node)
{
foreach ($node->findChildrenOfType('GotoStatement') as $goto) {
$this->addViolation($goto, array($node->getType(), $node->getName()));
}
}
}
```
如你所見,我們在上面例子中查詢字符串GotoStatement。PHPMD使用這個快捷注記定位具體PHP_Depend語法樹節點。所有PDepend抽象語法樹類都像這樣:`\PDepend\Source\AST\ASTGotoStatement`,其中`\PDepend\Source\AST\AST`是固定的,其他部分取決于節點類型。固定的這部分搜索抽象語法書節點時可以省略。你應該看看PHP_Depend's代碼包,可以找到所有當前支持的代碼節點,以便實現附加規則。
總結
在這篇文章中我們展示了實現PHPMD自定義規則的幾種方法。如果你認為你的規則可以被其他人或其他項目重用,不要猶豫,請提交你的自定義規則集到GitHub上項目的問題追蹤器,或是開啟pull請求。