PHPStan :PHP静态代码质量分析工具

PHP技术
343
0
0
2024-03-11

简介

为什么要用 PHPStan?

编译型语言需要在程序运行之前了解每个变量的类型,每个方法的返回类型。这就是为什么编译器需要确保程序是没有错误的,并且会在源码中向你指出这些类型的错误,比如调用了未定义的方法或者是向某个函数传递了错误数量的参数。在把应用程序部署到生产环境前,编译器算是第一道防线。

然而 PHP 就不会这样了。如果程序出错,会执行到错误的代码的时候崩溃。在测试 PHP 应用时,不管是自动化测试还是手动测试,开发人员都会花费大量时间去查一些其它编译型语言不会犯的错从而减少测试实际业务逻辑的时间。

PHP 是动态语言,不像静态语言那样有些错误可以直接在编译阶段发现,很多错误只有在线上运行了之后才会发现,这个时候可能已经对系统产生了影响。
PHPStan 是一款针对 PHP 语言的代码静态分析工具,它无需实际运行代码就可以发现其中的语法错误。如果你想我想改变这一点。那就请使用 PHPStan

PHPStan 是什么?

PHPStan 是一种用于 PHP 代码的静态分析工具。它是用 PHP 编写的,并于 2017 年首次发布。PHPStan 主要用于检测 PHP 代码中的错误,包括语法错误、类型错误、逻辑错误和安全漏洞。它还可以帮助开发者发现代码中可能存在的性能问题和可读性问题。

PHPStan 特点

  • 静态分析: PHPStan 是一款静态分析工具,这意味着它在运行 PHP 代码之前就会对其进行分析。这使得它能够检测到编译时错误,而无需实际运行代码。
  • 类型系统: PHPStan 拥有一个强大的类型系统,能够对 PHP 代码中的变量和函数进行类型检查。这有助于开发者发现代码中的类型错误,并确保代码的正确性。
  • 规则集: PHPStan 提供了一套丰富的规则集,用于检测代码中的错误。这些规则集涵盖了各种不同的方面,包括语法、类型、逻辑、安全和性能等。
  • 可配置性: PHPStan 允许开发者对规则集进行自定义,以满足他们的特定需求。这使得开发者可以只检测他们认为重要的错误,而忽略其他不重要的错误。
  • 集成: PHPStan 可以与各种不同的开发工具集成,包括 IDE、文本编辑器和构建工具等。这使得开发者可以在他们的日常开发工作中轻松地使用 PHPStan。
PHPStan 是一款非常流行的 PHP 代码分析工具,它已被许多公司和项目使用,包括 Facebook、Google、Netflix 和 WordPress 等。

PHPStan Level 体系

根据对语法检查的严格程度,PHPStan 划分了不同的级别 (level),目前共有 9 个级别,从 0 到 8,越来越严格。每个级别有不同的规则 (rule),这些规则描述了 PHPStan 会从哪些方面检查代码。对于新集成 PHPStan 的项目可以先使用最低级别,不至于面对大量的错误而无从下手。

使用

安装

要开始对代码执行分析,需要在 Composer 中使用 PHPStan

composer require --dev phpstan/phpstan
Composer 将在其 bin-dir 中安装 PHPStan 的可执行文件,默认为 vendor/bin

运行

为了让 PHPStan 分析你的代码库,你必须使用 analyse 命令并将其指向正确的目录。所以,这里直接用项目app目录,你可以像这样运行 PHPStan:

vendor/bin/phpstan analyse app
执行结果,发现了 6 处错误

我们查看第一处错误,打开编辑器查看common/security/Authorized.php文件

可以看出Tinywan\Casbin\Permission这个权限类确实是没有安装,应用了一个无效的类.

等我们完善以上的 6 个错误后,再次重新分析。将会提示[OK] No errors

可以看出默认级别是 0

the default and current level is 0
尝试调整级别为 1

可以看出级别1更加的严格

自定义规则

PHPStan 支持自定义规则,可以参考 https://github.com/phpstan/phpstan#custom-rules。

0~8 级别介绍

Level 0
  • 数组重复 $arr = ['id' => 1, 'id' => 1]; // error: Array has 2 duplicate keys with value 'id'
  • 使用空下标读取
$arr = ['id' => 1];
$id  = $arr[]; // error: Cannot use [] for reading.
  • 禁用内部函数
function foo()
{
  function bar() // error: Inner named functions are not supported by PHPStan. Consider refactoring to an anonymous function, class method, or a top-level-defined function.
  {
  }
}
  • 打印函数参数检查
sprintf('%s %s', 'foo'); // error: Call to sprintf contains 2 placeholders, 1 value given.
sscanf($str, '%d%d', $number); // error: Call to sscanf contains 2 placeholders, 1 value given.
  • 使用空下标读取
class Foo
{
  public static function foo()
  {
    $this->bar(); // error: Using $this in static method Foo::foo()
  }

  public function bar()
  {
  }
}
  • 检查函数实参数量是否和形参一致
  • 类属性可见性及是否存在
  • 未定义的函数
Level 1
  • 匿名函数未使用到的 use 引入的变量
$used   = 1;
$unused = 3;

function () use ($used, $unused) { // error: Anonymous function has an unused use $unused.
  echo $used;
};
  • 未定义的常量
  • 没有用到的构造函数参数
Level 2
  • 非法的类型转换
(string) new \stdClass(); // error: Cannot cast stdClass to string.
(int) []; // error: Cannot cast array() to int.
(int) 'blabla'; // error: Cannot cast 'blabla' to int.
  • 字符串中非法的变量类型
function foo(string $str, \stdClass $std)
{
  $s = "$str bar $std bar"; // error: Part $std (stdClass) of encapsed string cannot be cast to string.
}
  • 参数类型和默认值不兼容
function takesString(string $string = false): void
{
}
  • 非法的二元运算
function foo()
{
  5 / 0; // error: Binary operation "/" between 5 and 0 results in an error.
  1 + "blabla"; // error: Binary operation "+" between 1 and 'blabla' results in an error.
}
  • 非法的比较运算
function foo(stdClass $ob, int $a)
{
  $ob == $a;
  $ob != $a;
  $ob < $a;
  $ob > $a;
  $ob <= $a;
  $ob >= $a;
  $ob <=> $a;
}
Level 3
  • 往数组中添加类型错误的数据
class Foo
{
  /** @var int[] */
  private $integers;

  public function foo()
  {
    $this->integers[] = 4;
    $this->integers['foo'] = 5;
    $this->integers[] = 'foo'; // error: Array (array<int>) does not accept string.
  }
}
  • 数组操作符赋值
$value = 'Foo';
$value['foo'] = 1; // error: Cannot assign offset 'foo' to string.
  • 解包运算符操作对象是否可遍历
function foo(array $integers, string $str)
{
  $foo = [
  ...[1, 2, 3],
  ...$integers,
  ...$str // error: Only iterables can be unpacked, string given.
  ];
}
  • 生成器返回类型
/**
 * @return \Generator<string, int>
 */
function foo(): \Generator
{
  yield 'foo' => 1;
  yield 'foo' => 'bar'; // error: Generator expects value type int, string given.
}
  • 变量是否可复制
  • 属性类型
  • foreach 语句中的变量是否可遍历
  • 闭包函数返回类型
  • 箭头函数返回类型
  • 函数返回类型
Level 4
  • 数值比较结果恒定
function (int $i): void {
  if ($i > 5) {
    if ($i <= 2) { // error: Condition always false
    }
  }
};
  • 不会执行到的代码
function foo(bool $foo)
{
  if ($foo) {
    return;
  } else{
    return;
  }

  return $foo; // error: Unreachable
}
  • 无效的 catch 语句
function foo()
{
  try {
  } catch (\Throwable $e) {
  } catch (\TypeError $e) { // error: Dead catch - TypeError is already caught by Throwable above.
  }
}
  • 无效的方法调用
$arr1 = [1, 2];
$arr2 = [3, 4];
array_merge($arr1, $arr2); // error: Call to function array_merge() on a separate line has no effect.
  • 太宽泛的返回值类型声明
function bar(): ?string // error: Function bar() never returns string so it can be removed from the return typehint.
{
  return null;
}
Level 5
  • 函数实参类型
function foo(string $foo)
{
}

foo(1); // error: Parameter #1 $foo of function foo expects string, int given.
  • 形参为引用类型时实参必须为变量
function foo(&$foo)
{
}
 
$foo = 'foo';
foo($foo);
foo('foo'); // error: Parameter #1 $foo of function foo is passed by reference, so it expects variables only.
Level 6
  • PHPDoc 函数参数和代码中不一致
/**
 * @param int $a
 * @param int $b
 * @param int $c // error: PHPDoc tag @param references unknown parameter: $c
*/
function globalFunction($a, $b): void
{
}
  • PHPDoc 属性类型和代码不一致
  • PHPDoc 函数返回值类型和代码不一致
Level 7
  • 联合类型
/**
 * @param string|null $key
 * @return string|array<string>|null
 */
function foo($key = null)
{
  if (is_null($key)) {
    return null;
  } elseif (strpos($key, ',') !== false) {
    return explode(',', $key);
  } else {
    return $key;
  }
}
 
$len = strlen(foo('xx')); // error: Parameter #1 $string of function strlen expects string, array<string>|string|null given.
Level 8
  • 可能为空的值
/**
 * @property Author|null $author
 */
class Post {}
 
/**
 * @property string $name
 */
class Author {}
 
$post    = new Post();
$comment = $post->author->name; // error: Cannot access property $name on Author|null.