lvht/phpstan

PHPStan - PHP 静态分析工具

dev-master / 0.7.x-dev 2017-11-26 11:05 UTC

This package is not auto-updated.

Last update: 2024-09-15 02:46:48 UTC


README

为什么我想创建自己的分支?请参阅 故事

Build Status Latest Stable Version License PHPStan

PHPStan 通过不实际运行代码来查找错误。它甚至在您为代码编写测试之前就捕捉到整个类别的错误。

在运行实际代码行之前,PHPStan 可以检查代码中每行的正确性,这使 PHP 更接近编译语言。

在 Medium.com 上了解更多关于 PHPStan 的信息 »

PHPStan 是否帮助您在生产中避免错误?

它目前在您的代码上执行以下检查:

  • instanceofcatch、类型提示、其他语言构造甚至注解中类和接口的存在。PHP 不做此操作,而是保持沉默。
  • 在尊重分支和循环的作用域的情况下变量存在。
  • 被调用方法和函数的存在和可见性。
  • 访问的属性和常量的存在和可见性。
  • 属性分配的正确类型。
  • 构造函数、方法和函数传递的正确数量和类型参数。
  • 方法和函数返回的正确类型。
  • 基于格式字符串的 sprintf/printf 调用的正确参数数量。
  • 无用的类型转换,如 (string) 'foo'
  • 未使用的构造函数参数 - 可以删除它们,或者作者忘记在类代码中使用它们。
  • 只有对象才传递给 clone 关键字。

可扩展性

PHPStan 的独特功能是能够定义和静态检查类的“魔法”行为 - 访问在类中未定义但在 __get__set 中创建的属性,以及使用 __call 调用方法。

请参阅 类反射扩展动态返回类型扩展

您还可以安装已创建的特定框架的扩展

其他特定框架的扩展即将推出!

先决条件

PHPStan 需要 PHP >= 7.0。您必须在具有 PHP 7.x 的环境中运行它,但实际代码不必使用 PHP 7.x 特性。(为 PHP 5.6 及更早版本编写的代码在 7.x 上主要无需修改即可运行。)

PHPStan 与现代面向对象的代码配合得最好。您的代码类型越严格,您提供给 PHPStan 的工作信息就越多。

正确注释和类型提示的代码(类属性、函数和方法参数、返回类型)不仅有助于静态分析工具,还有助于其他与代码一起工作的人理解它。

安装

我们建议您下载 phpstan.phar

如果您想手动构建 phpstan.phar,则需要克隆项目,然后运行 composer update --no-dev,然后转到父目录并运行 phar-composer

要开始对您的代码进行分析,请在 Composer 中引入 PHPStan。

composer require --dev lvht/phpstan

Composer将在其bin-dir目录中安装PHPStan的可执行文件,默认为vendor/bin

首次运行

要让PHPStan分析您的代码库,您需要使用analyse命令,并将其指向正确的目录。

例如,如果您将类放在srctests目录中,您可以这样运行PHPStan

vendor/bin/phpstan analyse src tests

PHPStan可能会找到一些错误,但请不要担心,您的代码可能没有问题。首次运行发现的错误通常包括

  • 传递给函数的额外参数(例如,函数需要两个参数,代码传递了三个)
  • 传递给print/sprintf函数的额外参数(例如,格式字符串包含一个占位符,代码传递了两个值来替换)
  • 死代码中的明显错误
  • 需要定义的魔法行为。请参阅扩展性

在修复代码中的明显错误后,查看以下部分以获取所有配置选项,这将使报告的错误数量降至零,使PHPStan适合作为您持续集成脚本的一部分运行。

规则级别

规则级别。如果您想使用PHPStan,但您的代码库没有达到强类型和PHPStan严格检查的要求,您可以通过将--level传递给analyse命令来选择目前5个级别(0是最宽松的,4是最严格的)。默认级别为0

此功能使PHPStan检查的增量采用成为可能。您可以从较低的规则级别开始使用PHPStan,并在您觉得合适时增加它。

还有一个实验性级别5,目前可以启用

  • 联合类型(Foo|Bar将是一个指定类型,对其执行检查而不是混合类型)
  • 调用函数和方法时检查参数类型

自动加载

PHPStan使用Composer自动加载器,因此通过composer.json中的autoload/autoload-dev部分自动加载类是最简单的方法。

全局安装的自动加载

PHPStan支持使用composer global进行全局安装。在这种情况下,它不是项目自动加载器的一部分,但它支持从当前工作目录中的vendor/自动发现Composer自动加载器。

cd /path/to/project
phpstan analyse src tests # looks for autoloader at /path/to/project/vendor/autoload.php

如果您将依赖项安装在不同的路径或从不同的目录运行PHPStan,您可以使用--autoload-file|-a选项指定自动加载器的路径。

phpstan analyse --autoload-file=/path/to/autoload.php --autoload-file=/path/to/another.php src tests

排除分析文件

如果您的代码库中包含一些故意损坏的文件(例如,为了测试应用程序在包含无效PHP代码的文件上的行为),您可以使用--ignore-path|-P选项指定忽略路径:每行中的字符串用作fnmatch函数的模式。

phpstan analyse --ignore-path='tests/*/data/*' src tests

通用对象容器[待办事项]

没有预定义结构的类在PHP应用程序中很常见。它们用作通用数据持有者 - 任何属性都可以在它们上设置和读取。显著的例子包括stdClassSimpleXMLElement(这些默认启用),包含数据库查询结果的对象等。使用universalObjectCratesClasses数组参数让PHPStan知道您的代码库中使用了哪些具有这些特征的类

这需要另一个选项。

将非显然分配的变量添加到作用域[待办事项]

如果您在catch块中使用来自try块的一些变量,请将布尔参数polluteCatchScopeWithTryAssignments设置为true

try {
	$author = $this->getLoggedInUser();
	$post = $this->postRepository->getById($id);
} catch (PostNotFoundException $e) {
	// $author is probably defined here
	throw new ArticleByAuthorCannotBePublished($author);
}

如果您正在if-elseif分支中枚举所有可能的情况,并且PHPStan在条件之后抱怨未定义的变量,您可以在else分支中抛出异常

if (somethingIsTrue()) {
	$foo = true;
} elseif (orSomethingElseIsTrue()) {
	$foo = false;
} else {
	throw new ShouldNotHappenException();
}

doFoo($foo);

或者,您可以设置 defineVariablesWithoutDefaultBranch 布尔参数为 true 并保持代码如下:

if (somethingIsTrue()) {
	$foo = true;
} elseif (orSomethingElseIsTrue()) {
	$foo = false;
}

doFoo($foo);

我建议将 polluteScopeWithLoopInitialAssignmentspolluteCatchScopeWithTryAssignmentsdefineVariablesWithoutDefaultBranch 设置为 false,因为这会导致代码更清晰且易于维护。

自定义早期终止方法调用

前面的例子表明,如果某个条件分支以抛出异常结束,那么该分支不必定义条件分支结束后使用的变量。

但是,异常并不是提前终止方法执行的唯一方式。一些特定的方法调用也可能被项目开发者视为早期终止,例如停止执行的 redirect() 方法抛出内部异常。

if (somethingIsTrue()) {
	$foo = true;
} elseif (orSomethingElseIsTrue()) {
	$foo = false;
} else {
	$this->redirect('homepage');
}

doFoo($foo);

使用正则表达式忽略错误信息

如果您的代码库中存在一些难以修复的问题,或者只是简单想要稍后处理,您可以使用 --ignore-error|-E 选项指定忽略模式。

phpstan analyse --ignore-error='Call to an undefined method [a-zA-Z0-9\\_]+::method\(\)' \
	--ignore-error='Call to an undefined method [a-zA-Z0-9\\_]+::expects\(\)' \
	--ignore-error='Access to an undefined property PHPUnit_Framework_MockObject_MockObject::\$[a-zA-Z0-9_]+' \
	--ignore-error='Call to an undefined method PHPUnit_Framework_MockObject_MockObject::[a-zA-Z0-9_]+\(\)' \
	src tests

自定义规则

您可以使用 --level 选项使用预置规则来检查源代码。但是,如果您想要使用所有级别 n 规则,但排除 SomeBuiltInRule,您可以指定 --ignore-rule|-R 选项。

phpstan analyse --level=5 --ignore-rule='Methods\ReturnTypeRule' src tests

请注意,前面有一个斜杠,内置规则名称应该是相对名称!

使用 phpstan analyse --help 可以获取所有内置规则列表。

PHPStan 允许编写自定义规则来检查您自己的代码库中的特定情况。您的规则类需要实现 PHPStan\Rules\Rule 接口,并使用 --rule|-r 选项指定规则。

phpstan analyse --rule='\MyApp\PHPStan\Rules\DefaultValueTypesAssignedToPropertiesRule' src tests

请注意,前面有一个斜杠,自定义规则名称应该是完全限定名称(FQCN)!

关于如何实现规则,请参考 src/Rules,查看许多内置规则。

类反射扩展

PHP 中的类可以通过 __get__set__call 等类方法在运行时公开“魔法”属性和方法。因为 PHPStan 完全关于静态分析(在不运行代码的情况下测试代码错误),它必须事先知道这些属性和方法。

当 PHPStan 遇到一个内置类反射所不知道的属性或方法时,它会遍历所有注册的类反射扩展,直到找到一个定义了该属性或方法为止。

属性类反射扩展

此扩展类型必须实现以下接口

namespace PHPStan\Reflection;

interface PropertiesClassReflectionExtension
{

	public function hasProperty(ClassReflection $classReflection, string $propertyName): bool;

	public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection;

}

您可能还需要实现一个新的 PropertyReflection

namespace PHPStan\Reflection;

interface PropertyReflection
{

	public function getType(): Type;
	
	public function getDeclaringClass(): ClassReflection;

	public function isStatic(): bool;

	public function isPrivate(): bool;

	public function isPublic(): bool;

}

并且您可以使用 --extension|-x 来指定它

phpstan analyse --level=5 --extension='\App\PHPStan\PropertiesFromAnnotationsClassReflectionExtension' src tests

方法类反射扩展

此扩展类型必须实现以下接口

namespace PHPStan\Reflection;

interface MethodsClassReflectionExtension
{

	public function hasMethod(ClassReflection $classReflection, string $methodName): bool;

	public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection;

}

您可能还需要实现一个新的 MethodReflection

namespace PHPStan\Reflection;

interface MethodReflection
{

	public function getDeclaringClass(): ClassReflection;

	public function getPrototype(): self;

	public function isStatic(): bool;

	public function isPrivate(): bool;

	public function isPublic(): bool;
	
	public function getName(): string;

	/**
	 * @return \PHPStan\Reflection\ParameterReflection[]
	 */
	public function getParameters(): array;

	public function isVariadic(): bool;

	public function getReturnType(): Type;

}

并且您可以使用 --extension|-x 来指定它

phpstan analyse --level=5 --extension='\App\PHPStan\EnumMethodsClassReflectionExtension' src tests

动态返回类型扩展

如果方法的返回类型不总是相同的,而是取决于传递给方法的方法参数,您可以通过编写和注册扩展来指定返回类型。

因为您必须编写包含类型解析逻辑的代码,所以它可以是您想要的任何复杂性。

编写示例扩展后,变量 $mergedArticle 将具有正确的类型。

$mergedArticle = $this->entityManager->merge($article);
// $mergedArticle will have the same type as $article

这是动态返回类型扩展的接口

namespace PHPStan\Type;

use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;

interface DynamicMethodReturnTypeExtension
{

	public static function getClass(): string;

	public function isMethodSupported(MethodReflection $methodReflection): bool;

	public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type;

}

以下是正确解析 EntityManager::merge() 返回类型的扩展的编写方式

public static function getClass(): string
{
	return \Doctrine\ORM\EntityManager::class;
}

public function isMethodSupported(MethodReflection $methodReflection): bool
{
	return $methodReflection->getName() === 'merge';
}

public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
{
	if (count($methodCall->args) === 0) {
		return $methodReflection->getReturnType();
	}
	$arg = $methodCall->args[0]->value;
	$type = $scope->getType($arg);
	if ($type->getClass() !== null) {
		return $type;
	}

	return $methodReflection->getReturnType();
}

最后,您可以使用 --extension|-x 来指定它

phpstan analyse --level=5 --extension='\App\PHPStan\EntityManagerDynamicReturnTypeExtension' src tests

对于静态方法,也有类似的功能,使用 DynamicStaticMethodReturnTypeExtension 接口和 phpstan.broker.dynamicStaticMethodReturnTypeExtension 服务标签。

已知问题

  • 如果分析代码中使用 includerequire(而不是 include_oncerequire_once),PHPStan 将抛出 Cannot redeclare class 错误。使用 _once 变体来避免此错误。
  • 如果PHPStan在没有输出任何错误的情况下崩溃,很可能是因为您的系统设置了较低的内存限制。再次运行PHPStan,以获取一些提示,了解您可以采取哪些措施来防止崩溃。
  • 如果您在系统上全局安装PHPStan,您可能会遇到由于依赖项版本不同而导致的错误。例如,如果PHPStan使用的Symfony Console版本与您的版本不同,并且您在分析代码中使用此方法,PHPStan可能会将其标记为错误。这将在未来通过给PHPStan依赖项的命名空间添加前缀来解决。

行为准则

本项目遵循贡献者行为准则。通过参与本项目及其社区,您应遵守此准则。