
Vimeo 发布 PHP 静态分析工具 Psalm 7.0.0
来源标题:Psalm 7.0.0 [PHP static analysis tool by Vimeo]
Psalm 7.0.0 正式发布,带来大量新功能和缺陷修复。该版本要求运行在 64 位 PHP 8.2 至 8.6 上,多数项目升级流程简单;污点分析改用位掩码存储并基于函数紧凑摘要解析污点图,安全分析速度最高提升 10 倍。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
Psalm 7 发布公告!
本次发布包含大量新功能和错误修复,例如:
Psalm 7 可在 64 位 PHP 8.2 至 8.6 上运行。对大多数项目来说升级很简单:请参阅下方的“升级”部分。
污点现在以位掩码形式存储,污点图基于每个函数的紧凑摘要来解析。这使得安全分析速度提升最高可达 10 倍。
组合分析(#11384)在一次运行中同时执行类型检查、安全分析和死代码检测,因此污点分析现在默认开启(#11399)。
污点问题以 SECURITY 严重级别报告,并在 SARIF 报告中带有 rank(#12068、#11383)。结果不再因运行次数或线程数而变化(#11946、#11952、#11953)。
<?php
$run = function (string $c): void {
shell_exec($c);
};
$run((string) $_GET['cmd']); // TaintedShell,经由闭包传递
$mongo = new MongoDB\Driver\Manager('mongodb://localhost');
$mongo->executeQuery('db.users', new MongoDB\Driver\Query(['name' => $_GET['name']])); // TaintedNosql
/** @return mixed */
function body() {
return json_decode((string) file_get_contents('php://input'), true);
}
eval((string) body()); // TaintedEval:php://input 是污点源
在 psalm.dev 上试用 »
@psalm-pure、@psalm-mutation-free 和 @psalm-external-mutation-free 被 capabilities 取代,其建模参考了 Hack 的 contexts 和 capabilities。函数式声明会明确指定它可能产生哪些副作用:read-props、write-this-props、write-props、read-globals、write-globals、write-refs、io 和 time,两端分别是 pure 和 impure(#11984):
<?php
/** @psalm-capabilities time */
function today(): string {
return date('Y-m-d'); // 无问题
}
/** @psalm-pure */
function todayWrong(): string {
return date('Y-m-d'); // ImpureFunctionCall:... 需要 time
}
在 psalm.dev 上试用 »
新的 Purity Model 文章完整解释了该模型。
许多函数的纯度取决于传入的参数:当回调是纯的时,array_map() 就是纯的。纯度模板是模板参数,其值是 capability 集合而非类型,类似 Hack 的 [ctx $f] contexts。函数现在可以既是纯的又是高阶的,每次调用的代价仅取决于其参数的行为(#11984)。
借助 Closure[_],函数从其接收的闭包中获得纯度:
<?php
/**
* @psalm-pure
* @param Closure[_](int): int $f
*/
function apply(Closure $f, int $x): int {
return $f($x);
}
/** @psalm-pure */
function double(int $x): int {
return apply(fn(int $y): int => $y * 2, $x); // 无问题:闭包是纯的
}
/** @psalm-pure */
function noisy(int $x): int {
return apply(function (int $y): int { echo $y; return $y; }, $x); // ImpureFunctionCall:上下文是纯的,但对 apply 的调用需要 io
}
/** @psalm-pure */
function lengths(array $words): array {
return array_map(fn(string $w): int => strlen($w), $words); // 无问题:接收回调的内置函数同理
}
在类上,纯度模板让每个子类决定其方法可以做什么:
<?php
/** @psalm-purity-template P */
abstract class Handler {
/**
* @psalm-pure
* @psalm-purity-from-template P
*/
abstract public function handle(string $input): string;
}
/** @extends Handler[pure] */
final class Upper extends Handler {
/** @psalm-pure */
#[Override]
public function handle(string $input): string {
return strtoupper($input);
}
}
/** @extends Handler[io] */
final class Printer extends Handler {
/** @psalm-capabilities io */
#[Override]
public function handle(string $input): string {
echo $input;
return $input;
}
}
/** @psalm-pure */
function viaUpper(Upper $handler): string {
return $handler->handle('a'); // 无问题:Upper 将 P 绑定为 pure
}
/** @psalm-pure */
function viaPrinter(Printer $handler): string {
return $handler->handle('a'); // ImpureMethodCall:上下文是纯的,但方法 Printer::handle 需要 io
}
Traversable、Iterator、IteratorAggregate 和 Generator 都有一个纯度模板,用于描述遍历它们时的行为。iterable 也有一个。带有纯度注解的生成器会将其绑定到其函数体的行为:
<?php
/**
* @psalm-pure
* @param Traversable[pure]<int, int> $xs
*/
function sum(Traversable $xs): int {
$total = 0;
foreach ($xs as $x) { // 无问题
$total += $x;
}
return $total;
}
/**
* @psalm-pure
* @param Traversable<int, int> $xs
*/
function sumAny(Traversable $xs): int {
$total = 0;
foreach ($xs as $x) { // ImpureMethodCall:上下文是纯的,但遍历 Traversable<int, int> 需要 impure
$total += $x;
}
return $total;
}
/**
* @psalm-pure
* @return Generator<int, int>
*/
function numbers(): Generator {
yield 1;
yield 2;
}
/** @psalm-pure */
function total(): int {
return sum(numbers()); // 无问题:numbers() 返回 Generator[pure]
}
语法:纯度参数写在类型参数之前的方括号中,与 Hack 一致:Closureio: void、Handler[pure]、Iterator[pure]<int, string>、Box[io]<int>。具名模板用 @psalm-purity-template P 声明,用 @psalm-purity-from-template P 使用。
边界和默认值以链式写法表示:@psalm-purity-template write-this-props <= C(write-this-props) <= write-this-props|write-props|io。方法可以无条件使用下界。子类必须保持在边界之内,未绑定模板时则获得默认值。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。