- 开发工具
- 代码质量
- 质量保障
【免费下载链接】psalm
A PHP static analysis tool for finding errors and security vulnerabilities in PHP applications
本指南围绕 Psalm 的UnusedClass问题展开,讲解它在何种检测模式下被报告、底层基于代码引用图(CodeUseGraph)的死代码分析原理,以及正确区分「真实公共 API」与「死代码」并选用@api/@psalm-api注解修复的完整实战方案。读完本文,你将能准确解读UnusedClass报告,理解为什么@psalm-suppress无法替代@api,并能在自己的项目中正确配置死代码检测。
一、UnusedClass 是什么
UnusedClass是 Psalm 在开启死代码检测(--find-dead-code)后发出的一种问题报告:当 Psalm 在整个被分析代码库中找不到某个类的任何使用痕迹时,就会报告该类「从未被使用」。
以原文档给出的最小示例为例:
<?php final class A {} final class B {} $a = new A();在--find-dead-code模式下,Psalm 会发现类A被new A()实例化、属于「被使用」,而类B在整个代码库中没有任何引用,于是对B报告UnusedClass。这里类B就是典型的死代码。
从源码看,该问题的定义位于 src/Psalm/Issue/UnusedClass.php,它继承自ClassIssue,其ERROR_LEVEL为-2、SHORTCODE为75。ERROR_LEVEL为负值意味着该问题默认不参与常规的错误级别报告,只有在显式开启死代码检测时才会被产出,这与「UnusedClass仅在--find-dead-code下出现」的文档描述完全吻合。
二、如何开启死代码检测
UnusedClass属于 Psalm 的Unused*问题族,必须在死代码检测开启时才会被报告。开启方式有两种:
2.1 命令行参数
在运行 psalm 时追加--find-dead-code:
./psalm --find-dead-code从 src/Psalm/Internal/Cli/Psalm.php 的参数解析逻辑可以确认:
--find-dead-code会被转换为--find-unused-code=auto;- 若显式传入
--find-dead-code=always,则转换为--find-unused-code=always; --find-unused-code同样支持auto/always两种取值,传入always时无论代码是否被条件性引用都会执行查找。
其中auto模式只检查确实可以被分析的范围,而always模式会强制执行更彻底的死代码扫描。实际使用中,日常 CI 通常采用--find-dead-code(即auto)。
2.2 配置文件
也可以在psalm.xml中通过findUnusedCode属性全局开启:
<psalm findUnusedCode="true" >对应的 XML Schema 定义在 config.xsd,该属性类型为xs:boolean、默认值为true。需要说明的是,配置层面的findUnusedCode最终同样会走shouldFindUnusedCode()的auto分支(见 Psalm.php),因此开启后即可看到UnusedClass报告。
三、底层原理:Psalm 如何判断一个类「未被使用」
理解UnusedClass的关键在于理解 Psalm 的死代码检测算法。Psalm 并非简单地逐个类搜索引用,而是构建了一张代码使用关系有向图,然后从「根节点」出发做可达性分析。
3.1 代码使用图(CodeUseGraph)
核心实现位于 src/Psalm/Internal/Codebase/CodeUseGraph.php,其类注释明确说明了设计:
- 每个代码元素(类、函数、方法、属性、类常量)都是一个图节点,节点以
<kind> <member>字符串标识; - 每条边
A -> B的含义是「如果 A 是存活的,那么 B 被 A 使用」; - 「是否被使用」通过从一组根节点出发的可达性搜索来判定。根节点包括:公共 API、顶层文件代码、自由函数(Psalm 永不将自由函数报告为未使用)、以及项目之外的代码;
- 由于采用可达性分析,只被其他未使用代码引用的代码(包括互相引用的死代码环)也会被正确判定为未使用。
也就是说,Psalm 报告UnusedClass的依据不是「有没有字符串形式的引用」,而是「该类是否可以从根节点(真正活着的代码)出发被触达」。
3.2 报告位置与判定条件
UnusedClass的具体报告逻辑位于 src/Psalm/Internal/Codebase/ClassLikes.php。在开启$find_unused_code后,Psalm 遍历项目目录内的所有类,并执行这样的判断:
if ($code_use_graph->isUsed($class_node) && ($classlike_storage->public_api || $code_use_graph->isReferenced($class_node)) ) { // 类被使用:继续检查其方法、属性的引用 $this->checkMethodReferences($classlike_storage, $methods); $this->checkPropertyReferences($classlike_storage); } else { // 类未被使用:报告 UnusedClass IssueBuffer::maybeAdd( new UnusedClass( 'Class ' . $classlike_storage->name . ' is never used', $classlike_storage->location, $classlike_storage->name, ), $classlike_storage->suppressed_issues, ); }从这段源码可以提炼出两个关键判定因素:
- 图可达性:类节点是否从根节点出发被「使用」(
isUsed)或「引用」(isReferenced); - 公共 API 标记:类的存储信息中
public_api标志是否为真。
只有当「既未在图中被触达,又未被标记为公共 API」时,Psalm 才会生成UnusedClass。public_api标志正是由@api/@psalm-api注解驱动的(详见下文第四节)。类存储结构中的该字段定义于 src/Psalm/Storage/ClassLikeStorage.php。
四、如何修复:只有两条正确路径
当UnusedClass(或其他Unused*问题)被报告时,Psalm 文档明确指出只有两条正确的处理路径(见 supported_annotations.md):
该类确实属于公共 API—— 它会被被分析代码库之外的代码消费,例如:
- 库的入口类;
- 仅由框架、容器(DI 容器)或插件加载器实例化的类。
对于这类类,应使用
@api(或等价写法@psalm-api)标注。注意:该注解只应用于真正的公共面,不能为了消除告警而滥用。该类是死代码—— 它不存在任何外部消费者。此时应删除该类,以及所有仅通过它才能触达的代码(方法、属性、依赖它的其他类等),因为一旦删除入口,这些代码同样是不可达的死代码。
4.1 正确示例:标记公共入口类
<?php /** * 这个类被框架通过反射实例化,代码库内没有直接引用。 * * @psalm-api */ final class A {} final class B {} $a = new A();在加上@psalm-api之后,类A虽然代码库内没有任何new A(),但会被视为公共 API 而不再报告UnusedClass;类B仍会继续被报告,因为它既无引用、也无注解。
4.2@api与@psalm-api的解析
@api与@psalm-api是等价的两种写法。从源码看,Psalm 在解析类级 docblock 时对二者一视同仁(src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php):
$info->public_api = isset($parsed_docblock->tags['psalm-api']) || isset($parsed_docblock->tags['api']);随后该标记被写入类存储的public_api字段(见 ClassLikeNodeScanner.php),从而在 ClassLikes.php 的判定中让类直接通过「公共 API」这一分支、免于报告。方法与属性的@api解析逻辑与之类似(FunctionLikeDocblockParser.php)。
五、@api的完整使用规则
原文档引用了 supported_annotations.md 中的@api/@psalm-api完整指南,核心规则如下。
5.1 注解位置:优先标注整个类
对于被报告为未使用的方法或属性,@api可以放在该成员上,也可以放在整个类上。但几乎总是应该选择标注整个类:
- 一个被外部消费的成员,必然属于一个被外部消费的类;
- 一个类级别的注解即可覆盖其全部公共 API(类本身、所有公共方法、公共属性)。
反之,如果一个类没有@api标记、而类内某个成员被报告为未使用,那么该成员几乎总是真正的死代码 —— 正确做法是删除它,而不是单独给这个成员加注解。只有一种少见情况适合单独标注成员:类本身不是公共 API,但其中某一个成员确实是公共 API(例如类供内部使用、但某个回调/钩子方法被外部调用)。
5.2 外部接口实现自动视为公共表面
一个特殊且重要的规则:实现了项目外部定义的接口(或继承项目外部基类)的类,会被自动视为公共表面。原因在于这类类可以通过外部的类型被实例化和调用,Psalm 无法在项目内找到其全部消费方,因此其覆写方法会被视为「已使用」,无需显式@api(见 supported_annotations.md)。
5.3 不要在@api位置使用@psalm-suppress
@psalm-suppress UnusedClass(或其他Unused*抑制注解)不能替代@api。原因有二:
- 抑制注解只静默报告,并不会把类标记为「已使用」;
- 由于类未被标记为使用,它引用的其他符号仍然可能继续被报告为未使用,导致问题不断向下游蔓延。
在 supported_annotations.md 中对此有明确告诫。换句话说:抑制是「掩盖症状」,@api是「声明事实」,只有后者才能让整个引用链正确存活。
六、实战建议与判断清单
综合原文档、注解指南与源码实现,处理一条UnusedClass报告时可以按以下清单决策:
| 情况 | 判定 | 处理方式 |
|---|---|---|
| 类被框架/容器/插件加载器通过反射或魔法方式实例化 | 真实公共 API | 加@api/@psalm-api(标注整个类) |
| 类是库的对外入口、被项目外部代码使用 | 真实公共 API | 加@api/@psalm-api |
| 类实现了项目外部的接口/继承外部基类 | 自动视为公共表面 | 无需注解,确认无其他问题即可 |
| 类仅被项目内部代码使用,但内部引用也不存在 | 死代码 | 删除该类及其仅可达的关联代码 |
| 想消除告警但类既非公共 API 也非外部使用 | 死代码 | 不要用@psalm-suppress,应删除 |
值得强调的是:死代码检测的价值不仅在于清理无用类,更在于防止公共 API 面被误删。一个没有@api标记的库入口类,在--find-dead-code下会被报告为UnusedClass,如果开发者不加区分地删除,就会破坏库的对外契约。因此,为库的入口类和框架钩子类主动加上@api,是 Psalm 死代码检测实践中的关键习惯。
Psalm 仓库自身的插件测试也大量使用这一模式来声明「由反射实例化的插件类」——例如 tests/Config/Plugin/Hook/AfterAnalysis.php 中的@psalm-suppress UnusedClass registered as a plugin via test config, instantiated by reflection注释,即为通过抑制注解处理插件类未使用报告的现实案例(这也反向印证了:当类确实由反射加载、但又不想在代码库内声明其为公共 API 时,抑制是测试场景下的临时手段)。
七、相关参考
- 问题清单索引:docs/running_psalm/issues.md
@api/@psalm-api完整指南:docs/annotating_code/supported_annotations.md- 命令行参数详解:docs/running_psalm/command_line_usage.md
- 错误级别与问题配置:docs/running_psalm/error_levels.md
- 配置文件说明:docs/running_psalm/configuration.md
- 问题定义源码:src/Psalm/Issue/UnusedClass.php
- 死代码判定实现:src/Psalm/Internal/Codebase/ClassLikes.php
- 代码使用图实现:src/Psalm/Internal/Codebase/CodeUseGraph.php
@api注解解析:src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php
- 开发工具
- 代码质量
- 质量保障
【免费下载链接】psalm
A PHP static analysis tool for finding errors and security vulnerabilities in PHP applications
相关推荐
Psalm ParadoxicalCondition 问题详解:悖论条件的检测原理与修复实践
Psalm ParadoxicalCondition 问题详解:悖论条件的检测原理与修复实践 本文围绕 Psalm 官方文档 docs/running_psal
开发工具代码质量质量保障Psalm 死代码分析:PossiblyUnusedMethod 的触发原理、`@api` 正确修复与误报边界
Psalm 死代码分析:PossiblyUnusedMethod 的触发原理、 @api 正确修复与误报边界 导读 PossiblyUnusedMethod 是
开发工具代码质量质量保障Psalm 纯函数检查中的 ImpureVariable 问题:原理、触发场景与修复实践
Psalm 纯函数检查中的 ImpureVariable 问题:原理、触发场景与修复实践 ImpureVariable 是 Psalm 在分析纯函数( @psa
开发工具代码质量质量保障
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考