并发代码检查修复的问题

Lobsters Hottest 工具

摘要

本文详细阐述了在 ESLint 等代码检查工具中,并发应用多个修复可能引发代码错误的问题,并提出了通过顺序修复并重新分析来解决的方案。

<p><a href="https://lobste.rs/s/3rtqua/problem_with_concurrent_linter_fixes">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/26 11:12

# 并发代码检查修复的问题 来源:https://jfmengels.net/concurrent-linter-fixes/ 今天我想探讨代码检查工具(本文以JavaScript的ESLint为例,但远不止于此)中的一个问题:当检查器同时修复多个问题时,可能会引入一种在仅重新分析代码后才应用修复本不会出现的问题。 ## 问题描述(https://jfmengels.net/concurrent-linter-fixes/#the-problem) *(所有代码可在本仓库查看:https://github.com/jfmengels/eslint-concurrent-fixes-sscce)* 假设我们需要分析这个 `example.js` 文件: ```javascript let scores = [0, 100, 40, 60]; let averageScore = sum(scores) / scores.length; console.log(averageScore); function average(array) { return sum(array) / array.length; } function sum(array) { let sum_ = 0; for (const elem of array) { sum_ += elem; } return sum_; } ``` 这里可以发现两个改进点: - 我们将 `averageScore` 定义为 `scores` 的平均值,却在手动计算平均数,而本可以直接使用已有的 `average` 函数。 - `average` 函数目前未被使用,可以将其从源代码中移除。 问题在于:虽然这两个改进——在 `averageScore` 定义中使用 `average` 函数,以及移除 `average` 函数——各自都合理,但同时应用这两个修改会导致代码错误: ```javascript let scores = [0, 100, 40, 60]; let averageScore = average(scores); // ^^^^^^^ 未知引用 // 文件结尾,或仅保留 `sum` 的定义(取决于检查器应用修复的深度) ``` 在上述仓库中,我通过创建并启用两个检查规则来演示此问题: - `sscce/useAvailableUtils`:将代码替换为同一文件中可用的工具函数(虽然这类规则看似有用,但该规则极度简化且仅为本示例定制) - `sscce/removeUnusedFunctions`:报告并自动删除未被引用的函数 *(这两个规则编写得较为简单,几乎无法用于实际项目,但足以用于我们分析的这个简单示例。)* ## 处理过程(https://jfmengels.net/concurrent-linter-fixes/#walkthrough) 启用这两个规则后会发生什么?检查器(此处为ESLint)对 `example.js` 运行所有规则的分析,确定有两个可用的修复方案: - 来自 `sscce/useAvailableUtils` 的自动修复:将 `sum(scores) / scores.length` 替换为 `average(array)` - 来自 `sscce/removeUnusedFunctions` 的自动修复:移除 `average` 函数的定义 由于这两个修复**仅在编辑范围上**没有冲突,检查器同时应用了它们,然后重新分析发现 `sum` 函数现在也可以被移除了。 ## 可能的解决方案(https://jfmengels.net/concurrent-liter-fixes/#possible-solution) 我曾向Nicholas C. Zakas(https://github.com/nzakas)(ESLint的创建者)请教过这个问题,他告诉我虽然这确实可能发生,但实际上问题不大。我倾向于认同,因为实践中我既未遇到此问题也未听说相关投诉。但我确实认为,如果ESLint更积极地删除未使用代码(例如让 `no-unused-vars`(https://eslint.org/docs/latest/rules/no-unused-vars)支持自动修复)并为其他规则添加自动修复功能,这个问题可能会更突出。 虽然我认同这是罕见情况,但在开发我为Elm语言编写的检查器 `elm-review`(https://elm-review.com/)时,这种可能性始终让我警惕。因此我在其中做了一个我认为更合理(虽然更慢)的选择:每次仅应用一个修复,然后重新分析代码。 以下是两者的区别: ESLint的工作流程: 1. 分析项目 2. 批量应用所有报告错误中的可用修复 3. 丢弃剩余错误并返回步骤1,直到没有可修复的错误 `elm-review`的工作流程: 1. 分析项目 2. 应用报告错误中的第一个可用修复 3. 丢弃剩余错误并返回步骤1,直到没有可修复的错误 这确实意味着检查器分析项目的时间可能比必要时间更长,但确保在任何时刻都只应用对文件/项目有意义的修复。 在此示例中,如果第一个可用修复(顺序可能随机)是使用 `average` 函数,那么在第二次迭代中 `average` 将被视为已引用而不会被删除。最终结果将是: ```javascript let scores = [0, 100, 40, 60]; let averageScore = average(scores); console.log(averageScore); function average(array) { return sum(array) / array.length; } function sum(array) { let sum_ = 0; for (const elem of array) { sum_ += elem; } return sum_; } ``` 反之,如果第一个可用修复是删除 `average` 函数,那么第二次迭代中将没有 `average` 函数可用,`averageScore` 将保持不变: ```javascript let scores = [0, 100, 40, 60]; let averageScore = sum(scores) / scores.length; console.log(averageScore); function sum(array) { let sum_ = 0; for (const elem of array) { sum_ += elem; } return sum_; } ``` 两个版本都是合理的,除非检查器为任一修复优先级更高,否则两者都是对原始代码的改进。真正错误的是试图同时应用两个修复导致产生损坏代码。 ## 结论(https://jfmengels.net/concurrent-linter-fixes/#conclusion) 这个问题可能出现在任何对代码进行重要修改的规则组合中,只要这些修改单独应用100%正确: - 代码删除 - 重命名 - 移动语句或表达式 - 等等 当希望为检查器添加更大胆的自动修复功能时(例如ESLint在自动修复中通常不会删除大量代码),我认为此问题是需要优先解决的。 我选择的解决方案——在修复之间重新分析代码——有明确的性能代价。遗憾的是,在我无法控制规则的情况下(因为 `elm-review` 和ESLint都支持工具维护者未知的自定义规则),我尚未找到更好的解决方案。不支持自定义规则的检查器(我认为这是一个关键缺失功能)或许可以通过优先级等方式预先识别冲突。 我很享受完全不用考虑这个问题的感觉。我不必担心我的规则修复可能在特定情况下与另一个规则的修复冲突(此处描述的意义),也不必为可能遇到此类问题的用户花费大量时间调试。

相似文章

技能成为新的代码检查工具

Reddit r/AI_Agents

作者认为,使用AI技能来自动化代码质量检查,会重现代码检查工具最初旨在解决的内存与可靠性问题,从而质疑基于大语言模型的技能作为替代方案的有效性。