#如何为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请求。