lll-lll-lll-lll/depone

GitHub: lll-lll-lll-lll/depone

一款 PHP 静态分析工具,通过将 require_once 语句与 Composer autoload 规则交叉比对,帮助开发者安全识别并清理冗余的文件加载调用。

Stars: 3 | Forks: 0

# depone [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/lll-lll-lll-lll/depone/actions/workflows/ci.yml) [![最新版本](https://img.shields.io/packagist/v/depone/depone)](https://packagist.org/packages/depone/depone) [![PHP 版本](https://img.shields.io/packagist/dependency-v/depone/depone/php)](https://packagist.org/packages/depone/depone) [![许可证](https://img.shields.io/packagist/l/depone/depone?cacheSeconds=3600)](LICENSE) 老旧的 PHP 代码库中包含数百行 `require_once`,而这些在多年前就已经被 Composer 的 autoload 淘汰了。手动删除它们是一种冒险:有些是冗余代码,有些掩盖了损坏的 autoload 规则,还有些加载的类副本与 autoloader 加载的*不同*。 `depone` 会读取你代码库中的每一个 PHP 文件,静态解析每个 `require_once` 的目标,并在一条命令中区分这些情况,无需任何配置,也绝不修改你的代码: - **冗余 (redundant)** — 目标已经被 autoload 加载;删除该 require 在可证明上是安全的 - **冲突 (conflicting)** — require 加载了一个被遮蔽的副本;删除它会改变实际加载的类定义 ![depone 发现冗余的 require_once 语句](https://static.pigsec.cn/wp-content/uploads/repos/cas/f9/f9ef71e25c44b03fd89665937b8d8b841db17aed7ec1a532ea357196066dfc4f.gif) ## 安装说明 需要 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, 云安全监控, 开发工具, 技术债务, 静态分析