☰
Psalm 的 PossiblyUnusedParam 检查:未使用方法参数检测与下划线抑制机制详解
2026/10/12 20:39:07 网站建设 项目流程
  • 开发工具
  • 代码质量
  • 质量保障

【免费下载链接】psalm

A PHP static analysis tool for finding errors and security vulnerabilities in PHP applications

项目地址:https://gitcode.com/gh_mirrors/ps/psalm
点击查看免费下载

Psalm 是一款面向 PHP 的静态分析工具,用于在 PHP 应用中查找错误与安全漏洞。当开启死代码检测(--find-dead-code/--find-unused-code)时,Psalm 会报告那些声明了但从未在方法体内被引用的参数,其中公共(public)与受保护(protected)方法对应PossiblyUnusedParam,私有(private)方法与普通函数则对应更确定的UnusedParam。本文以 docs/running_psalm/issues/PossiblyUnusedParam.md 为骨架,结合仓库源码与测试用例,深入讲解该问题的触发条件、与UnusedParam的差异、报错信息格式、下划线命名抑制机制以及配置层面的开关方式,帮助你在实际项目中精准治理"占位参数"。

问题触发条件:何时会报告 PossiblyUnusedParam

PossiblyUnusedParam在满足以下条件时被报告:

  • 开启了死代码检测(--find-dead-code或--find-unused-code,等价于配置项findUnusedCode);
  • 被检查的成员是public 或 protected方法(非接口方法);
  • 方法体内从未引用某个形式参数;
  • 该方法未被其他方法覆写(没有overridden_method_ids)。

原文档给出的最小复现示例:

<?php class A { public function foo(int $a, int $b) : int { return $a + 4; } } $a = new A(); echo $a->foo(1, 2);

运行检测命令:

vendor/bin/psalm --find-dead-code

此时 Psalm 会在foo方法的$b参数处报告PossiblyUnusedParam,提示文本为"Param #2 is never referenced in this method"(参数编号从 1 开始)。仓库测试 tests/UnusedCodeTest.php 给出了完全一致的断言:

'possiblyUnusedParam' => [ 'code' => '<?php class A { /** @return void */ public function foo(int $i) {} } (new A)->foo(4);', 'error_message' => 'PossiblyUnusedParam - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:49 - Param #1 is never referenced in this method', ],

从该测试可见,实际报告格式为PossiblyUnusedParam - 文件路径:行:列 - Param #N is never referenced in this method,其中N是 1 起始的参数序号。

与 UnusedParam 的区别:public/protected 与 private/函数

仓库中存在一对"姊妹"问题:PossiblyUnusedParam(SHORTCODE 134)与UnusedParam(SHORTCODE 135),二者在 src/Psalm/Issue/ 目录下定义,ERROR_LEVEL均为-2(参见 src/Psalm/Issue/PossiblyUnusedParam.php 与 src/Psalm/Issue/UnusedParam.php)。ERROR_LEVEL = -2表示该问题属于"始终报错"类别——即不随 error level(1 最严格、8 最宽松,默认 2)升降而被降级为 info,只能通过下划线命名或@psalm-suppress等方式抑制,无法用调整错误级别的方式掩盖。

两者的分派逻辑集中在 src/Psalm/Internal/Codebase/ClassLikes.php 的checkMethodParamReferences方法中:

  • 若方法可见性不是private且类不是接口,则遍历每个参数:
    • 方法被标记为final时报告UnusedParam;
    • 否则报告PossiblyUnusedParam("Possibly" 表示该方法未来可能被子类覆写或通过外部 API 使用该参数,因此语义上偏"可能未使用"而非"确定未使用");
  • 满足以下任一条件则跳过,不报告:
    • 方法已被覆写(overridden_method_ids非空);
    • 参数无位置信息(location为空);
    • 参数是构造器属性提升参数(promoted_property);
    • 参数经FileReferenceProvider::isMethodParamUsed判定为在代码库中被引用过。

与之相对,docs/running_psalm/issues/UnusedParam.md 说明UnusedParam针对private 方法或普通函数:

<?php function foo(int $a, int $b) : int { return $a + 4; }

以及 docs/running_psalm/issues/UnusedClosureParam.md 说明闭包参数对应UnusedClosureParam:

$a = function (int $a, int $b) : int { return $a + 4; };

同类"死代码族"问题还包括PossiblyUnusedMethod、PossiblyUnusedProperty、PossiblyUnusedReturnValue、UnusedClass、UnusedForeachValue等(见 docs/running_psalm/issues/ 目录),它们都由同一套未使用代码检测框架驱动。

为什么是 "Possibly":覆写与外部引用的不确定性

对于非 final 的 public/protected 方法,参数未在方法体内使用并不代表"永远没用"——子类可能覆写该方法并消费该参数,或者框架/第三方代码通过反射等方式以特定签名调用它。因此 Psalm 对这类方法使用偏保守的PossiblyUnusedParam:

  • 方法若存在覆写关系(overridden_method_ids非空),checkMethodParamReferences会直接跳过整个方法的参数检查;
  • 只有final方法才升级为确定性的UnusedParam;
  • 检测逻辑还会借助 src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php 的detectUnusedParameters,在函数体分析层面核对:抽象方法跳过、父类(非抽象)同位置存在参数时跳过——即只要父类实现使用了该位置参数,子类方法即使自己不用也不会被报告。

同时,Psalm 6 起未使用代码检测默认开启(findUnusedCode默认true),因此在没有显式传参的情况下也可能收到该报告。文档 docs/running_psalm/configuration.md 明确说明:

  • findUnusedCode="true"(默认)→ 检测所有未使用代码;
  • findUnusedVariablesAndParams="true"→ 仅检测未使用变量与参数(等价于--find-unused-variables);
  • 可在 psalm.xml 中显式关闭:<psalm findUnusedCode="false">。

命令行层面,src/Psalm/Internal/Cli/Psalm.php 显示--find-dead-code会被归一化为--find-unused-code(可选值auto/always,默认auto),随后由reportUnusedCode开启整条未使用代码检测链路。

下划线前缀:推荐的抑制方式

原文档给出的标准抑制方法是将未使用参数名加上下划线前缀:

<?php class A { public function foo(int $a, int $_b) : int { return $a + 4; } }

这是 Psalm 官方推荐的做法,其底层实现在 src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php 的isIgnoredForUnusedParam方法中:

private function isIgnoredForUnusedParam(string $var_name): bool { return str_starts_with($var_name, '$_') || (str_starts_with($var_name, '$unused') && $var_name !== '$unused'); }

即符合以下任一规则的参数名会被豁免:

  • 以$_开头(如$_b、$_request);
  • 以$unused开头且不等于单独的$unused(如$unusedParam、$unusedFlag)。

该方法在detectUnusedParameters(src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php)与detectPreviousUnusedArgumentPosition(同文件 L2747)两处均被调用,保证这些参数既不会被报告,也不会影响"前一个有效参数"的定位计算。

参数命名建议:让签名自解释

由于$_前缀只是让 Psalm 忽略检查,实际代码中建议遵循以下约定,兼顾工具与可读性:

  • 必须实现接口/抽象方法签名时:实现方往往必须保留某些参数(例如事件监听器、策略模式的统一签名),此时用$_前缀明确表达"这是约定的占位参数";
  • 框架回调/钩子方法:参数位置由框架约定固定,即使当前实现用不到也需保留,$_命名让后续维护者一眼看出"该参数有意留空";
  • 避免误用:如果参数其实"应该"被使用(只是暂时遗漏),$_前缀会掩盖真实的逻辑缺陷。优先考虑是否真的不需要它,再决定是否抑制;
  • 团队一致性:$_前缀是 Psalm 与 PHP 社区通用惯例($unused*变体亦受支持),在团队规范中统一即可。

其他抑制途径:@psalm-suppress 与基线

除下划线命名外,Psalm 还提供通用抑制机制:

  • 行内抑制:在问题行上方使用@psalm-suppress PossiblyUnusedParam,也可在方法 docblock 中抑制(见 docs/running_psalm/dealing_with_code_issues.md)。注意:抑制只对该符号本身生效,不会把该符号变成可达入口,因此仅被它引用的其他未使用代码仍会被报告(docs/running_psalm/configuration.md 有明确说明);
  • 基线(baseline):一次性生成当前所有问题的基线文件并逐步清零:
vendor/bin/psalm --set-baseline vendor/bin/psalm --use-baseline=psalm-baseline.xml

详见 docs/running_psalm/dealing_with_code_issues.md。仓库根目录的 psalm-baseline.xml 即是 Psalm 自身项目使用的基线示例。

两种方式相比,$_前缀把"意图"固化在源码里(自文档化),而@psalm-suppress与基线只是压制报告;对于可能被覆写的 public/protected 方法,推荐优先使用$_前缀。

实践清单:让 PossiblyUnusedParam 保持可控

  1. 先定位:开启死代码检测,vendor/bin/psalm --find-dead-code,关注Param #N is never referenced in this method报告;
  2. 区分场景:public/protected 非 final 方法 →PossiblyUnusedParam;final/private 方法或函数 →UnusedParam;闭包 →UnusedClosureParam;
  3. 决策:若参数确实无用且为签名占位,加$_前缀;若只是遗漏使用,应补充逻辑而非抑制;
  4. 团队规范:约定$_/$unused*命名规则,配合 phpcs.xml 等代码风格工具保持一致性;
  5. 兜底:遗留问题较多时用基线逐步收敛,最终目标是无新增、无残留。

通过理解PossiblyUnusedParam的触发条件、Possibly语义背后的覆写与外部引用考量,以及下划线抑制的精确实现,你可以在启用 Psalm 死代码检测后,以最贴合源码意图的方式处理"未使用参数",既保持 API 签名兼容,又让代码库干净可维护。

  • 开发工具
  • 代码质量
  • 质量保障

【免费下载链接】psalm

A PHP static analysis tool for finding errors and security vulnerabilities in PHP applications

项目地址:https://gitcode.com/gh_mirrors/ps/psalm
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询