☰
Psalm 死代码检测:UnusedClass 问题的原理、触发条件与 `@api` 修复实践
2026/10/13 2:19:33 网站建设 项目流程
  • 开发工具
  • 代码质量
  • 质量保障

【免费下载链接】psalm

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

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

本指南围绕 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, ); }

从这段源码可以提炼出两个关键判定因素:

  1. 图可达性:类节点是否从根节点出发被「使用」(isUsed)或「引用」(isReferenced);
  2. 公共 API 标记:类的存储信息中public_api标志是否为真。

只有当「既未在图中被触达,又未被标记为公共 API」时,Psalm 才会生成UnusedClass。public_api标志正是由@api/@psalm-api注解驱动的(详见下文第四节)。类存储结构中的该字段定义于 src/Psalm/Storage/ClassLikeStorage.php。

四、如何修复:只有两条正确路径

当UnusedClass(或其他Unused*问题)被报告时,Psalm 文档明确指出只有两条正确的处理路径(见 supported_annotations.md):

  1. 该类确实属于公共 API—— 它会被被分析代码库之外的代码消费,例如:

    • 库的入口类;
    • 仅由框架、容器(DI 容器)或插件加载器实例化的类。

    对于这类类,应使用@api(或等价写法@psalm-api)标注。注意:该注解只应用于真正的公共面,不能为了消除告警而滥用。

  2. 该类是死代码—— 它不存在任何外部消费者。此时应删除该类,以及所有仅通过它才能触达的代码(方法、属性、依赖它的其他类等),因为一旦删除入口,这些代码同样是不可达的死代码。

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

项目地址:https://gitcode.com/gh_mirrors/ps/psalm
点击查看免费下载
上一篇:BiliDownloader:一键解锁B站视频离线收藏神器
下一篇:5大核心功能解锁:OpenVINO™ AI插件为Audacity带来智能音频处理新体验

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

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

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

立即咨询