AI 无视 lint 报错继续用废弃语法 —— 排查与修复

AI 生成的代码用了废弃 API,看到 lint 报错改了一处,下一个文件又把废弃写法塞回来。需要把规则锚定到项目层。

你两年前就禁掉了 componentWillMount,ESLint 也标记它,代码库里早就零使用。Cursor 生成一个新组件,悄悄又用了 componentWillMount。你指出 lint 报错,AI 说”好的我马上修”,改完那个文件。三个 prompt 之后,在另一个完全无关的组件里,componentWillMount 又冒出来了。AI 看得到 lint 规则,也表示同意,然后在跨轮次时直接忘掉。同样的剧情在 var、ESM 项目里的 require()、React 16 风格的 useEffect 清理逻辑、pg.Pool.connect 回调而非 Promise 上一遍遍上演。模式很清楚:AI 训练数据里仍然充斥着废弃形式,lint 反馈只在当前轮次有效,你的护栏没有触达真正需要的地方。

常见原因

按真实会话中出现频次排序。

1. AI 训练时见到的大量代码都还没废弃这个 API

GitHub 上公开代码里大部分还是旧写法。模型见过一百万次 componentWillMount,只见过一千次 componentDidMount,概率自然偏向旧的。

如何识别:在与你修过的位置毫无关联的其他文件里,同一个废弃写法反复出现。

2. lint 反馈只覆盖单次轮次

Cursor 或 Claude Code 读到 lint 输出,把那一行改掉,接着进入下一个任务——规则并没有进入长期记忆。

如何识别:同一条 lint 规则跨会话反复触发;“修复”只在同一对话内生效。

3. 项目层没有规则告诉 AI 什么是被禁的

.cursorrulesCLAUDE.md 或 system prompt 里压根没提到这条废弃。AI 没有任何信号说明这个代码库比公网更严格。

如何识别:查规则文件——如果废弃模式没出现在里面,AI 就完全没有理由避开它。

4. ESLint 把规则设为 “warn” 而不是 “error”

规则存在,但只 warn 不 error。AI 跑完 lint 看到”通过了”,其实是被警告淹没了。

如何识别:eslint --max-warnings 0 失败,但单跑 eslint 退出码是 0。

5. 废弃是项目自定义规则,AI 无从知晓

你用自定义规则禁了生产代码里的 console.log。AI 要等到跑 lint 才知道有这条规则,即便看到了也未必理解为什么禁。

如何识别:lint 输出里的规则名,在 AI 的解释里从来没被提到过。

6. 库升级后悄悄废弃了旧写法

你把 pg 从 v7 升到 v8,回调写法被废弃。AI 训练数据 v7 和 v8 各占一半,挑哪个全看”感觉”。

如何识别:npm ls <package> 显示是较新的主版本;废弃写法对应的是上一个主版本。

开始之前

  • 把当前在意的所有废弃模式列清楚,写下来:“我们永远不用 X,总是用 Y”。
  • 跑一遍 eslint --max-warnings 0,确认你的规则确实按 error 而非 warning 在执行。
  • 看一下 .cursorrules / CLAUDE.md 当前内容。
  • 抓住一个 AI 重新引入废弃写法的实例,保存文件名和前后 prompt。

需要收集的信息

  • 废弃模式(如 componentWillMountvarrequire()、回调 API)。
  • 替代写法(正确做法)。
  • 应该触发的 lint 规则名(react/no-deprecatedno-var 等)。
  • 该规则在 .eslintrc / eslint.config.js 中的启用状态与严重度。
  • .cursorrules / CLAUDE.md / AGENTS.md 的内容。
  • 如果废弃和库相关,库的版本号。

分步修复

按从快速见效到长期策略排序。

第 1 步:把 lint 规则改成硬性 error

eslint.config.js.eslintrc 里:

{
  "rules": {
    "react/no-deprecated": "error",
    "no-var": "error",
    "import/no-commonjs": "error"
  }
}

CI 强制零警告:

eslint . --max-warnings 0

这样 AI 那句”我跑了 lint,通过了”就真的会在废弃写法上失败。

第 2 步:在规则文件里写明禁止项

仓库根目录的 .cursorrulesCLAUDE.md:

## Banned patterns (do NOT generate)

- componentWillMount, componentWillReceiveProps, componentWillUpdate
  → use componentDidMount + useEffect equivalents
- var → use const, let
- require(), module.exports → use ESM import / export
- pg client callback API → use the promise / async-await form
- console.log in src/ outside of src/lib/logger.ts

If you generate any of the above, the lint check WILL fail.
Verify your code does not contain these patterns before responding.

这能把禁止项注入到 AI 每一轮的工作上下文里。

第 3 步:用正例告诉 AI 正确写法长什么样

加一份 docs/conventions.md,左右对照,然后在规则文件里引用它:

# Banned vs preferred

## React lifecycle

WRONG:
  componentWillMount() { this.fetch(); }

RIGHT:
  useEffect(() => { fetch(); }, []);

## Module system

WRONG:
  const x = require("foo");

RIGHT:
  import x from "foo";

具体对照比抽象禁令有效得多——AI 会模仿 RIGHT 那边的样式。

第 4 步:把 lint 放进 pre-commit hook

package.json:

{
  "scripts": {
    "lint": "eslint . --max-warnings 0"
  },
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": "eslint --max-warnings 0"
  }
}

配 husky / lefthook 等 pre-commit hook。提交时的强制校验 AI 绕不过去。

第 5 步:能 --fix 的就自动修

很多废弃规则自带 autofix(no-varprefer-constprefer-arrow-callback 等):

eslint . --fix

每次 AI 生成完跑一遍,免去人工抓回归的认知负担。

第 6 步:大范围迁移用 codemod

回调转 Promise、class 转 hooks、CommonJS 转 ESM 这种大动作,用 jscodeshiftts-morph:

npx jscodeshift -t ./codemods/cb-to-promise.js src/

codemod 跑完后,代码库里废弃写法的样本归零——AI 对你代码库做 RAG 时,只看得到新写法,自然会模仿新风格。

验证

  • eslint . --max-warnings 0 在整个仓库零警告退出。
  • 让 AI 在 3 个不同 prompt 下生成新文件——没有一个再引入废弃写法。
  • grep -r "<deprecated-pattern>" src/ 全仓零命中。
  • 故意提交一段废弃写法,pre-commit hook 用清晰错误信息拦下。

长期预防

  • CLAUDE.md / .cursorrules 里维护”deprecations”小节,每次升级主版本依赖时回顾。
  • ESLint、TSC、自定义 AST linter 对废弃 API 统一设为 error——warning 是 AI 会忽略的训练噪声。
  • 每条禁令都配上正确替代写法,不要光禁不教。AI 对”这样写”远比对”别那样写”敏感。
  • 定期 grep -r "<deprecated>" src/ 清理漏网之鱼;AI 工具非常忠实地模仿仓库现状。
  • 升级主版本时,专门拉一个 PR 用 codemod 把废弃 API 在整个仓库扫一遍。混合状态既迷惑人也迷惑 AI。
  • 简要写下每条废弃的”为什么”。AI 对”componentWillMount 在并发模式下不安全”的锚定效果,远好于一条干巴巴的禁令。

常见误区

  • 把规则停在 “warn” 级别。AI 看到 lint 通过就以为自己干完了。
  • 禁令只写进自己的笔记,而不是 AI 真正会读的规则文件。
  • 在一个文件里修了废弃写法,却没 grep 整个仓库——通常还埋着 5 到 10 处。
  • 以为 AI 会从本次对话内的纠正里”学到”,于是不写规则文件。跨会话不持久,只对同一对话的后续轮次有效。
  • 同一个文件里新旧风格混杂。AI 会挑离它最近的那种风格沿用下去。

相关问题见 AI 建议了过时的依赖AI 引入的 TypeScript 错误Claude Code 缺失项目上下文

FAQ

Q:我一直在纠正它,它一直犯同样的错,是我哪里没做对吗?

纠正只在当前对话有效。把规则搬到 .cursorrules / CLAUDE.md,新会话才会继承。对话内提醒只是短期记忆。

Q:lint 规则已经启用了,AI 还是生成废弃写法,为什么?

AI 是”先生成 → 再跑 lint → 再修当前轮次的文件”的流程。如果你在 lint 跑之前就接受了代码,或之后才手动 lint,废弃形式就溜走了。把 lint 做成 pre-commit 硬卡口。

Q:我直接把废弃 API 从库里删掉行不行?

如果库由你掌控,这是最干净的做法。第三方库就不行了,只能靠规则文件 + lint 强制。

Q:怎么让 AI 主动推荐现代写法,而不是只是不用旧的?

每条禁令都配上一个具体的 RIGHT 示例。AI 模仿正面样本的能力远强于尊重抽象禁令。

标签: #排查 #AI 编程 #deprecation #eslint #linting