
Psalm 7 发布:PHP 安全与静态分析工具改用能力模型替代纯度注解
来源标题:Psalm 7 release: PHP security&static analysis software
Psalm 7 发布,用基于 Hack 上下文与能力模型的新机制取代 @psalm-pure 等旧注解,函数可精确声明读属性、写全局、IO、时间等副作用。新增纯度模板,使 array_map 等高阶函数可随回调保持纯性,Closure[_] 可继承闭包纯度。
以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。
@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'); // no issue
/** @psalm-pure */
function todayWrong(): string {
return date('Y-m-d'); // ImpureFunctionCall: ... requires 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); // no issue: the closure is pure
/** @psalm-pure */
function noisy(int $x): int {
return apply(function (int $y): int { echo $y; return $y; }, $x); // ImpureFunctionCall: The context is pure but function call on apply requires io
/** @psalm-pure */
function lengths(array $words): array {
return array_map(fn(string $w): int => strlen($w), $words); // no issue: builtins taking callbacks work the same way
在类上,纯度模板让每个子类决定其方法可以做什么:
<?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'); // no issue: Upper binds P to pure
/** @psalm-pure */
function viaPrinter(Printer $handler): string {
return $handler->handle('a'); // ImpureMethodCall: The context is pure but method Printer::handle requires io
Traversable、Iterator、IteratorAggregate 和 Generator 具有纯度模板,用于描述遍历它们时的行为。iterable 也有一个。带有纯度注解的 generator 将其绑定到其函数体的行为:
<?php
/**
* @psalm-pure
* @param Traversable[pure]<int, int> $xs
*/
function sum(Traversable $xs): int {
$total = 0;
foreach ($xs as $x) { // no issue
$total += $x;
return $total;
/**
* @psalm-pure
* @param Traversable<int, int> $xs
*/
function sumAny(Traversable $xs): int {
$total = 0;
foreach ($xs as $x) { // ImpureMethodCall: The context is pure but iterating over Traversable<int, int> requires 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()); // no issue: numbers() returns a 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。方法可以无条件使用下界。子类必须保持在边界内,未绑定模板时获得默认值。
构造函数可以通过其参数绑定类模板,使用 Closure[P] 参数或 __construct 上的条件 @return。例如,new ArrayObject([...]) 是 ArrayObject[pure],因此纯函数可以填充它创建的 ArrayObject(#12116、#12179、#12190)。
纯度模板会在重写、trait 和嵌套在模板化作用域中的闭包中进行检查(#12015、#12126、#12027)。
同样支持推断:Psalm 会推断对模板化函数的调用的纯度,--alter 会为函数调用的闭包参数添加 Closure[_](#12077、#12019)。
接受回调或迭代器的内置函数和 SPL 用纯度模板声明:array_map、usort、preg_replace_callback、iterator_to_array、CallbackFilterIterator、Fiber 等(#12122)。
参见 Purity Model 文章中的 Purity templates。
未使用的代码现在在引用图上查找(#11939)。代码只有在可从入口点到达时才被视为已使用,因此仅被其他未使用代码引用的类和方法(包括循环引用)也会被报告。创建但从未抛出的异常也会被报告(#12487):
<?php
final class Service {
public function run(int $id): void {
if ($id < 0) {
new InvalidArgumentException('negative id'); // UnusedMethodCall: ... never thrown or used
echo $id;
final class Ping { // UnusedClass
public static function ping(): void { Pong::pong(); }
final class Pong { // UnusedClass
public static function pong(): void { Ping::ping(); }
(new Service())->run(1);
在 psalm.dev 上试试 »
--find-unused-psalm-suppress 现在还会检查类和 Tainted* 问题上的抑制。
(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。