lll-lll-lll-lll/depone
GitHub: lll-lll-lll-lll/depone
一款 PHP 静态分析工具,通过将 require_once 语句与 Composer autoload 规则交叉比对,帮助开发者安全识别并清理冗余的文件加载调用。
Stars: 3 | Forks: 0
# depone
[](https://github.com/lll-lll-lll-lll/depone/actions/workflows/ci.yml)
[](https://packagist.org/packages/depone/depone)
[](https://packagist.org/packages/depone/depone)
[](LICENSE)
老旧的 PHP 代码库中包含数百行 `require_once`,而这些在多年前就已经被 Composer 的 autoload 淘汰了。手动删除它们是一种冒险:有些是冗余代码,有些掩盖了损坏的 autoload 规则,还有些加载的类副本与 autoloader 加载的*不同*。
`depone` 会读取你代码库中的每一个 PHP 文件,静态解析每个 `require_once` 的目标,并在一条命令中区分这些情况,无需任何配置,也绝不修改你的代码:
- **冗余 (redundant)** — 目标已经被 autoload 加载;删除该 require 在可证明上是安全的
- **冲突 (conflicting)** — require 加载了一个被遮蔽的副本;删除它会改变实际加载的类定义

## 安装说明
需要 PHP 8.1+ 和 Composer。
```
composer require --dev depone/depone
```
## 快速开始
在 Composer 项目的根目录下运行分析器:
```
vendor/bin/depone
```
以一个典型的老旧前端控制器为例,它是在引入 Composer autoload 之前编写的:
```
say();
```
在项目根目录下运行 `depone`:
```
redundant_require_once=2
public/index.php:4 => src/Greeting.php
public/index.php:5 => src/Legacy/Mailer.php
conflicting_require_once=0
unresolved_include_require=1
public/index.php:7 [variable] $config['plugins_dir'] . '/bootstrap.php'
```
第 4-5 行声明的类,Composer 已经会从这些相同的文件中 autoload 它们,因此可以删除。第 6 行(`src/helpers.php`)*没有*被报告:它定义的是辅助函数而不是类,所以 autoload 永远不会覆盖它,这个 require 是必不可少的。第 7 行从变量构建其路径,因此 `depone` 无法静态解析它——它会被报告为未解析,而不是被静默忽略,并被排除在冗余列表之外。第 3 行对 `vendor/autoload.php` 的 `require_once` 永远不会被报告:它本身不属于可 autoload 的集合,其目标可解析但并不冗余。
在删除针对 `Mailer.php` 的 `require_once` 之前,请确认还有谁依赖它:
```
vendor/bin/depone --trace src/Legacy/Mailer.php
```
```
trace_target=src/Legacy/Mailer.php
direct_callers=1
- public/index.php
entrypoint_candidates=1
- public/index.php
trace_paths=1
1. public/index.php -[r]-> src/Legacy/Mailer.php
```
由于只有一个调用者和清晰的追踪路径,这两行冗余代码可以被安全删除,并交由 Composer 的 autoloader 处理。
## 理解报告
每一个其目标能被静态解析的 `require_once` 都会归入以下三类之一:
| 部分 | 含义 | 处理方式 |
| --- | --- | --- |
| `redundant_require_once` | 目标已经被 autoload,删除该 require 在可证明上不会改变任何东西。 | 删除该 require。 |
| `conflicting_require_once` | 目标声明了一个类,但 Composer 会从*另一个*不同的文件 autoload 该类。删除该 require 会改变加载的定义。 | 在修改 require 之前,先调查被遮蔽的副本。 |
| *(无 —— 静默)* | 目标合理地不可被 autoload:没有匹配的规则、推导路径缺失的类、未声明类型,或者还定义了函数/常量及执行了顶层代码。 | 保留它;该 require 是必不可少的。 |
只有当删除某个 require 在可证明上是安全时,它才会被称为 `redundant`:目标是一个预加载的 `autoload.files` 条目,或者它只声明了类类型(没有函数、常量或副作用),并且它声明的*每一个*类都会 autoload 回同一个文件。`conflicting` 会被标记出来,但绝不会被视为可随意删除的内容。
路径表达式无法被静态解析的 Include/require 语句绝不会被静默跳过——它们会被列在 `unresolved_include_require` 下并附带原因:
| 原因 | 含义 |
| --- | --- |
| `variable` | 表达式包含变量 |
| `method_call` | 表达式包含对象方法调用 |
| `static_access` | 表达式包含 `::` 访问 |
| `complex` | 评估器无法解析的其他任何内容 |
## JSON 输出
传入 `--format json` 可以将报告作为 JSON 而非文本摘要输出——这对于 CI 仪表板、编辑器或通过管道传递给 `jq` 非常有用。退出代码保持不变;仅输出格式有所不同(`text` 是默认值)。
```
vendor/bin/depone --format json
```
```
{
"redundant": [
{ "file": "public/index.php", "line": 4, "target": "src/Greeting.php" },
{ "file": "public/index.php", "line": 5, "target": "src/Legacy/Mailer.php" }
],
"conflicting": [],
"unresolved": [
{ "file": "public/index.php", "line": 7, "type": "require_once", "reason": "variable", "expr": "$config['plugins_dir'] . '/bootstrap.php'" }
]
}
```
冗余行包含 `file`、`line` 和 `target`;冲突行会额外增加一个 `detail` 字符串。`--format json` 同样适用于 `--trace`,会将追踪结果输出为 JSON 对象(`target`、`directCallers`、`entrypoints`、`paths`、`truncated`)。非有效 UTF-8 的字符串(从老旧的非 UTF-8 源码中提取的表达式)会将其无效字节替换为 U+FFFD,以确保报告始终完好无损。
## 退出代码
| 代码 | 含义 |
| --- | --- |
| `0` | 分析运行完成,未发现冗余或冲突的 require。 |
| `1` | 分析运行完成,并报告了至少一个冗余或冲突的 require。 |
| `2` | 分析无法运行(无法读取的 `composer.json`、未知选项等)。 |
`unresolved_include_require` 条目会被报告,但绝不会影响退出代码:老旧的动态 include 通常是合理的,如果因此报错,几乎会使每个老旧项目的首次运行都以失败告终。`--trace` 的输出仅供参考,除非分析本身失败,否则始终返回 `0`。
## 在 CI 中使用
由于发现结果会返回非零退出代码,一旦出现冗余或冲突的 require,一个普通的步骤就会使构建失败:
```
- run: composer install --no-progress
- run: vendor/bin/depone
```
要在不因发现结果而导致构建失败的情况下打印报告,请使用:
```
vendor/bin/depone || [ $? -ne 2 ]
```
这会忽略发现的结果(退出代码 `1`),但在分析完全无法运行时(退出代码 `2`)仍会使步骤失败。普通的 `|| true` 也会掩盖执行错误——即使根本没有进行分析,步骤也会显示通过。
## 工作原理
1. 加载 autoload 规则。当 Composer 已经生成了 autoloader(存在 `vendor/composer/autoload_*.php`)时,depone 会使用这些生成的映射表:它们将根项目与每个已安装的依赖项合并,这与 Composer 在运行时解析它们的方式完全一致,因此依赖项提供的类也能被识别。如果没有生成的 autoloader,它将回退到直接读取根目录的 `composer.json`(`psr-4`、`psr-0`、`classmap` 和 `files`,包括它们对应的 `autoload-dev`),并使用 Composer 自带的 class-map 生成器扫描 `classmap` 条目。无论哪种方式,类名都通过 Composer 自身的 `ClassLoader` 解析为文件。
2. 通过使用 `token_get_all()` 对每个文件进行 token 化处理,找到每一个 require/include(排除 `vendor/` 和 `.git/`),并使用 php-parser 的常量表达式评估器来评估路径表达式,该评估器根据分析上下文进行了扩展,支持:被分析文件的 `__DIR__`/`__FILE__`、`define()` 定义的常量以及 `dirname()` 调用。无法被解析的表达式将被报告为 `unresolved_include_require`,而不是被丢弃。
3. 对于每个已解析的 `require_once` 目标,使用 [nikic/php-parser](https://github.com/nikic/PHP-Parser) 解析该文件,以查找其声明的类类型,并根据第 1 步中的 autoload 规则检查每个声明的类:
- **冗余 (redundant)** — 目标是一个预加载的 `autoload.files` 条目,或者它只声明了类型,并且*每个*声明的类都会解析回该文件。删除它可证明是安全的。
- **冲突 (conflicting)** — 一个声明的类会从*另一个*不同的文件被 autoload,因此该 require 加载了一个被遮蔽的副本。
- 否则,该 require 将不会被报告(没有匹配的规则、推导路径缺失的声明类、未声明类型,或者文件还定义了函数/常量或执行了顶层代码——autoload 无法重现这些内容,因此该 require 是必不可少的)。
## 与 PHPStan 和 Rector 的关系
depone 并不能替代 PHPStan、Psalm 或 Rector——请在它们旁边配合使用。它涵盖了它们没有对应规则的一个狭窄领域:将每个 `require_once` 与 Composer autoload 联系起来——目标是已经被 autoload 了,还是遮蔽了被 autoload 的副本?这是一个路径解析+ autoload 匹配的问题,而不是 AST 转换,这就是为什么它作为一个独立的小型工具存在的原因。`composer dump-autoload
--strict-psr`/`--strict-ambiguous` 会自行验证 autoload 配置,但从不解析源代码级别的 `require_once` 语句,因此无法建立这种联系。此外,depone 在设计上仅用于生成报告,绝不会重写你的代码。
## 范围与稳定性
`depone` 是一个 CLI 工具。其支持的公开接口包括命令名称、选项、退出代码和命令输出。`src/` 下的 PHP 类是内部实现细节,可能会在不保证向后兼容性的情况下发生更改。
## 开发说明
```
git clone git@github.com:lll-lll-lll-lll/depone.git
cd depone
composer install
composer check # php-cs-fixer + phpstan + phpunit
```
使用 `composer cs-fix` 来应用 PHP-CS-Fixer 的更改。
## 为什么叫 "depone"?
源自拉丁语 *Rerum curam depone* —— “放下你的担忧”。这正是它对老旧代码库中 `require_once` 代码行所做的事情:带着证据,一个接一个地把它们放下。
## 许可证
[MIT](LICENSE)
标签:ffuf, OpenVAS, PHP, SOC Prime, 云安全监控, 开发工具, 技术债务, 静态分析