eslint配置实践和踩坑

245 阅读14分钟

Q: 如果我的eslintrc是这样配置的:

module.exports = {
  extends: [
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended',
    'plugin:prettier/recommended',
    '@repo/eslint-config/strict',
    '@repo/eslint-config/react',
    '@repo/eslint-config/typescript',
    '@repo/eslint-config/prettier',
  ],
  rules: {
    // 'react/jsx-uses-vars': 'error',
  },
};

那么多个extends的结果,是jsonmerge覆盖吗 还是相同的配置会被deep merge?

Answer:

ESLint 在处理 extends 时,行为不是一个“深度 merge”,而是有明确的合并/覆盖规则:

1. 规则(rules)的合并方式

  • 各个 extends 里的 rules 最终会合并成一个对象

  • 同一个规则如果在多个配置里都出现,后面的会覆盖前面的(即后者优先级更高)。

  • 合并是 浅层级的,不会做 deep merge。例如:

    // 第一个 extends
    { rules: { 'no-unused-vars': ['error', { vars: 'all' }] } }
    
    // 第二个 extends
    { rules: { 'no-unused-vars': ['warn'] } }
    

    最终结果就是 { 'no-unused-vars': ['warn'] },前一个的 { vars: 'all' } 整个丢掉。

2. 其他配置项的合并

  • parserOptionssettings 等对象字段:同样是 浅合并,同名 key 后者覆盖前者。
  • plugins:会做合并去重(数组拼接,不会覆盖)。
  • env:多个配置里的布尔值会合并(true 会“叠加”)。

3. 你的例子里

extends: [
  'plugin:react/recommended',
  '@yhdfe/eslint-config/strict',
  '@yhdfe/eslint-config/react',
  '@yhdfe/eslint-config/typescript',
  '@yhdfe/eslint-config/prettier',
],
  • 最终生效的配置是按顺序从前到后合并的。
  • 如果 plugin:react/recommended 打开了 react/jsx-uses-vars,但你后面的某个包(例如 @yhdfe/eslint-config/react)把它关掉了,那最终就是“被关掉”。
  • 如果你在 rules 里再手动写一遍,那就是 最终权威值(最高优先级)。

4. 怎么验证?

你可以直接运行:

npx eslint --print-config index.js

它会输出 index.js 最终生效的完整配置(合并后的结果),这样就能直观看到 react/jsx-uses-vars 是什么状态。

总结

  • extends 的合并是 浅合并,同名规则或设置,后者覆盖前者。
  • pluginsenv 这类会做合并,rulesparserOptionssettings 这类就是浅覆盖。
  • 想知道最终结果,用 --print-config 最靠谱。

Q:我有一个index.js文件里,有这样的代码:

import React from 'react';
import ReactDom from 'react-dom';
import ToastContainer from './ToastContainer';

const toastContainerDiv = document.createElement('div');
document.body.appendChild(toastContainerDiv);

const getToastContainerRef = () => {
  return ReactDom.render(<ToastContainer />, toastContainerDiv);
};

看代码的话,ToastContainer这个变量有被调用,但是报了eslint error:'ToastContainer' is defined but never used.eslint(no-unused-vars)。

以下是我的eslint配置文件:

require("@rushstack/eslint-patch/modern-module-resolution");

module.exports = {
    parser: '@babel/eslint-parser',
    parserOptions: {
        sourceType: 'module',
        ecmaFeatures: {
            jsx: true,
        },
        requireConfigFile: false,
        babelOptions: {
            presets: ['@babel/preset-react'],
        },
    },
    plugins: ['react', 'react-hooks'],
    settings: {
        react: {
            version: 'detect',
        },
    },
    rules: {
        // eslint built-in rules
        "no-param-reassign": 2, // 禁止对函数参数进行重新赋值
        "no-prototype-builtins": 0, // 允许使用Object.prototype的内置方法
        // eslint-plugin-react规则
        "react/display-name": 0, // 允许组件缺少displayName属性
        "react/jsx-key": 1, // 警告缺少key属性的jsx元素
        "react/no-array-index-key": 1, // 警告在jsx元素中使用数组索引作为key
        "react/prop-types": 0, // 允许缺少propTypes属性
        "react/react-in-jsx-scope": 0, // 允许在jsx中使用React变量而不导入React模块
        "react/self-closing-comp": 1, // 警告没有子元素的自闭合组件
        // eslint-plugin-react-hooks规则
        "react-hooks/exhaustive-deps": 1, // 警告useEffect和useCallback的依赖项不完整
        "react-hooks/rules-of-hooks": 2, // 强制遵守React Hooks的规则
        // eslint-plugin-jsx-a11y规则
        "jsx-a11y/accessible-emoji": "off", // 关闭使用emoji的警告
        "jsx-a11y/click-events-have-key-events": 0, // 允许点击事件没有对应的键盘事件
        "jsx-a11y/no-static-element-interactions": 0, // 允许静态HTML元素没有点击事件时不需要增加role属性
        "jsx-a11y/anchor-is-valid": [
            "warn",
            {
                components: ["Link"],
                specialLink: ["hrefLeft", "hrefRight"],
                aspects: ["invalidHref", "preferButton"],
            },
        ], // 警告无效的锚点链接和推荐使用按钮代替的情况
        "jsx-a11y/anchor-has-content": [
            0,
            {
                components: ["Anchor"],
            },
        ], // 允许锚点元素没有内容
    },
};

分析一下为什么会有这个eslint的误报,要如何修复?

Answer:
好——我一步步把可能的原因和可行的修复都列清楚,结论先说在最前面:

结论(最可能) :ESLint 的核心规则 no-unused-varsToastContainer is defined but never used,是因为 ESLint 默认不会把 JSX 中使用的组件名 当作“已使用的变量”来识别。eslint-plugin-react 提供了把 JSX 用法标记成“已使用变量”的规则(react/jsx-uses-vars / 以及历史上的 react/jsx-uses-react),你当前的配置没有启用这类规则(或插件/版本未生效),所以才会误报。

下面按步骤分析与修复(包含可选方案、调试检查项和最小改动示例)。

一、为什么会发生(详细分析)

  1. ESLint core 的 no-unused-vars 规则只看“变量在 AST 中是否被引用”,但旧版/纯核心并不会把 JSX 元素 <ToastContainer /> 的出现自动视为对 ToastContainer 变量的引用。

  2. eslint-plugin-react 提供了两个帮助类规则来解决这个问题:

    • react/jsx-uses-react —— 告诉 ESLint React(旧的 JSX runtime)在 JSX 中被使用(用于避免 React 被误判为未使用)。
    • react/jsx-uses-vars —— 告诉 ESLint “以大写开头的 JSX 标识符在 JSX 中是被使用的变量”,避免组件名被 no-unused-vars 误判。
  3. 你的配置里虽然有 plugins: ['react', 'react-hooks']settings.react.version 也有 detect,但你并没有显式启用 react/jsx-uses-vars,也没有 extends: ['plugin:react/recommended'] 之类去自动打开这些关联规则。因此 ESLint 仍然把 ToastContainer 视作“定义但未使用”。

另外可能的次要原因(排查项):

  • eslint-plugin-react 没有真实安装(plugins 写了但依赖缺失) → 插件规则不会生效。
  • 使用了不兼容的插件/ESLint 版本(插件老或者 ESLint 太老),导致规则名不存在或被忽略。
  • ESLint 缓存导致旧报错仍显示(--no-cache 可排查)。

二、推荐的最小修复(优先推荐)

在你现在的 .eslintrcrules加一条

// 在现有 module.exports.rules 内添加
"react/jsx-uses-vars": "error",

完整示例片段(只展示关键位置):

module.exports = {
  parser: '@babel/eslint-parser',
  parserOptions: { /* ...你原来的配置... */ },
  plugins: ['react', 'react-hooks'],
  settings: { react: { version: 'detect' } },
  rules: {
    "react/jsx-uses-vars": "error",

    // 你原来的规则...
    "no-param-reassign": 2,
    "no-prototype-builtins": 0,
    // ...
  },
};

这样做后 eslint 会把 <ToastContainer /> 的使用识别为对 ToastContainer 的引用,从而消除 no-unused-vars 的误报。

三、其他可选方案(依据场景选择)

A. 使用插件的推荐配置(会带来更多 React 规则)

extends: [
  'plugin:react/recommended'
],

优点:自动启用一批推荐规则(包含避免 JSX 误报的规则)。缺点:会启用很多规则,可能需要额外调整。

B. 如果你使用 React 17+ 的新 JSX 转换(automatic runtime)
eslint-plugin-react 提供 plugin:react/jsx-runtime 的 extend(取决于插件版本)。如果你使用新 transform,可以考虑加上它。但最简洁仍是单独打开 react/jsx-uses-vars

C. 文件级/行级忽略(不推荐,临时可用)
在 import 那行加注释:

import ToastContainer from './ToastContainer'; // eslint-disable-line no-unused-vars

只用于临时屏蔽报错,不推荐作为长期解决方案。

四、调试步骤(如果上面修复不生效,请按这个顺序检查)

  1. 确认 eslint-plugin-react 真安装:npm ls eslint-plugin-reactpnpm ls eslint-plugin-react
  2. 清除 ESLint 缓存后重跑:eslint --no-cache index.js
  3. 打印 eslint 生效的配置查看最终是否包含规则:npx eslint --print-config index.js,看输出中是否包含 "react/jsx-uses-vars" : "error"
  4. 若 ESLint 或 plugin 版本较旧,考虑升级 eslinteslint-plugin-react 到较新的稳定版本(通常能避免兼容性问题)。

五、补充说明(容易混淆的点)

  • 你把 react/react-in-jsx-scope 设为 0(允许 JSX 时不导入 React)这和 ToastContainer 的误报无关。react-in-jsx-scope 只是控制 React 是否必须在文件中声明。
  • react/jsx-uses-react(标记 React 被使用)跟 react/jsx-uses-vars(标记 JSX 元素名被使用)是两个不同作用的规则;这里我们关心后者。

六、快速结论与建议

  • 最简单、最小改动:在 ESLint 配置里加入 "react/jsx-uses-vars": "error",然后重跑 ESLint。
  • 若你愿意接受更多 React 相关检查,改用 extends: ['plugin:react/recommended']
  • 如果仍不生效,请确认 eslint-plugin-react 已安装且 ESLint 不是用缓存运行,并用 --print-config 检查最终配置。

Q:eslint 从v7升级到v8之后,有哪些升级点,应用配置上会有什么变化?

🔑 核心升级点 (v7 → v8)

1. Node.js 支持范围收紧
  • v7 支持 Node.js 10+
  • v8 开始要求 Node.js 12.22.0+ / 14.17.0+ / 16+
    ➡️ 如果你项目里 CI 或本地环境还在 Node 10,会直接报错。
2. 旧版解析器 (espree) & 解析参数调整
  • v8 内置的 Espree 升级到 ECMAScript 2022 支持。
  • parserOptions.ecmaVersion 默认值改为 latest(v7 默认是 2020)。
    ➡️ 配置里如果写死了低版本的 ecmaVersion,可能导致一些新语法不被识别。
3. 插件与配置加载机制更严格
  • 插件必须显式安装在当前项目里,不能再依赖“跨包透传”。
  • v8 去掉了 CLIEngine(已废弃),官方推荐用 ESLint API (ESLint class)
    ➡️ 影响一些工具库(如 eslint-loader、旧版 lint-staged 的适配)。
4. 规则变化
  • 大量规则废弃 / 重命名:例如

    • indent-legacy → 移除
    • no-loss-of-precision 新增
  • 一些规则默认更严格,比如 no-unused-vars 在 TypeScript 项目里经常会冲突,需要通过 @typescript-eslint/no-unused-vars 替代。

5. 配置文件差异
  • .eslintrc.* 系列依然可用(兼容 v7 写法)。
  • v8 开始引入 Flat Config (实验)eslint.config.js,但需要额外参数启用,不是默认。
    ➡️ 也就是说 v8 里你还可以沿用 v7 的 .eslintrc.js,迁移成本很低。
6. CLI 使用变化
  • --init 命令生成的配置默认会倾向于现代化(比如推荐 Prettier、TypeScript 支持)。
  • --resolve-plugins-relative-to 行为更明确,避免从错误目录解析插件。
7. @typescript-eslint 相关生态同步升级
  • 如果你用 TypeScript,@typescript-eslint/eslint-plugin@typescript-eslint/parser 必须升到 5.x 才能兼容 ESLint v8。
    ➡️ 这是业务里最常见的坑:ESLint v8 + TS 旧插件 (v4.x) 会直接报错。

🛠️ 应用配置上的变化

  1. Node 版本检查

    • 确保 CI/CD、开发机 Node ≥ 12.22,否则 ESLint v8 直接跑不起来。
  2. 规则差异

    • 部分规则在 v8 变严格或废弃,升级后要跑一遍 eslint . --fix 看看破坏性。
    • 对于 TS 项目,建议把 no-unused-vars 等替换成 @typescript-eslint 系列。
  3. 插件依赖

    • 必须在项目的 package.json 明确安装需要的插件和 parser。
    • 不再允许依赖于共享配置(比如 @yhdfe/eslint-config)去间接拉插件,得业务自己装。
  4. 配置文件

    • .eslintrc.js 写法在 v8 完全兼容,你可以继续用,不需要立刻迁移 Flat Config。
    • 如果未来考虑升级到 v9,再逐步迁移到 eslint.config.js
  5. 工具链兼容性

    • 检查 eslint-loadereslint-webpack-pluginlint-staged 等依赖版本,旧版可能和 v8 API 不兼容。

✅ 总结

  • v7 → v8 升级是“平滑 + 小坑” :大部分配置不用改,主要是 Node 版本 + TS 插件升级。
  • 配置层面.eslintrc.js 写法还能用,不需要立刻改 Flat Config。
  • 风险点:规则变动、插件依赖要装全,TypeScript 项目要同步升级 @typescript-eslint

Q:再帮我总结一下eslint v8升级到v9的变化,包含eslint本身的升级点,和应用配置的变化?

从 ESLint v8 升级到 v9 的主要变化(包括 ESLint 本身的新特性 / breaking changes)以及 在使用/配置上可能受到影响的点

我会先列出 v9 的新特性 / breaking 变动,然后指出在配置/插件/迁移上你可能要注意的地方。

(资料主要参考官方发布说明)(eslint.org)

一、ESLint v9 的关键变化 / 新特性 / Breaking 改动

下面是 v9 相对于 v8 的主要新增或变更:

变化 / 特性说明影响或注意点
Flat Config 成为默认 / 配置系统升级在 v9.0 中,新的配置体系(flat config)被设为默认配置模式。(eslint.org)你可以选择迁移到 eslint.config.js / eslint.config.ts 的 flat 配置格式。若继续使用旧的 .eslintrc.* 格式仍然支持,但新生态(插件 / 规则 /共享配置)可能更多倾向 flat config。
API / 规则插件兼容性改动为了支持 flat config,新版本做了一些规则 API 更改。部分插件(如 eslint-plugin-react 等)在 v9 中需要升级以适配新的 API。(GitHub)如果你用的某些插件还没在 v9 上更新,就可能出现 context.getScope is not a function 等错误(这在社区中已有用户反馈)(GitHub)
TypeScript 语法支持增强在 v9 的后续小版本里,ESLint 核心规则对 TypeScript 语法的支持逐步增强,比如 no-restricted-globalsno-var 等规则扩展以支持 TS 类型注释场景。(eslint.org)如果你的代码里混合 TS/JS,升级后可能部分规则在 TS 文件里能工作得更好,不再完全依赖 @typescript-eslint 的覆盖。
CLI / 运行时工具增强- 支持 TypeScript 形式的 ESLint 配置 (eslint.config.ts)(实验特性,需要启用标志)(eslint.org) - 新增一些 CLI 选项(例如 --pass-on-unpruned-suppressions)(eslint.org) - 支持新的语法(例如 ECMAScript 的 Explicit Resource Management 语法)(eslint.org)这些增强给你更多灵活性,但也意味着你要检查工具链是否支持这些新选项 / 新配置形式。
生态 / 插件升级压力由于配置系统变动与 API 变化,很多插件 / 共享配置要同步适配 v9 才能正常工作。社区已有部分插件在 v9 上出现兼容性问题。(eslint.nodejs.cn)升级时必须逐个检查项目里用到的插件 / 共享配置是否兼容 v9,否则可能 lint 报错或行为异常。
核心规则废弃 / 删除 / 调整虽然 v9 没做像 v8 那样大范围废弃规则,但因为配置系统 / API 变化,一些规则内部实现可能改变或移除旧 API 依赖你的自定义规则或共享配置里如果用了某些内部 API,可能需要调整。

二、在配置 / 项目中的变更与注意点

下面是你在升级到 v9 时在项目配置、插件兼容、迁移等方面要重点关注和处理的内容:

关注点 / 步骤要做 / 检查可能出现的问题 / 提示
升级 ESLint 版本package.json 里把 eslint 升为最新 v9 版本某些旧版本依赖或工具(如 eslint-loader、旧版 lint-staged 插件)可能不再兼容,需一并升级
升级插件 / 共享配置检查项目里使用的 eslint-plugin-xxxeslint-config-xxx@typescript-eslint 等是否有 v9 兼容版本如果插件版本太旧,可能报错如 “context.getScope is not a function” 等错误 (GitHub)
决定是否迁移到 Flat Config你可以继续使用 .eslintrc.js 等旧格式,也可以逐步迁移到 eslint.config.js / eslint.config.ts 的 flat 配置若迁移,应重写配置结构(不再用 extends,而是直接配置数组 + plugin 引入)
兼容性处理 / 适配层为了兼容还未支持 flat config 的共享配置或插件,可能需要使用兼容层或工具(如 @eslint/compat@eslint/eslintrc迁移过程中可能显得配置有点混杂,需逐步替换为原生 flat config 支持的插件 / 配置
自定义规则 / 插件兼容性如果你或团队自己写过自定义 ESLint 规则、插件,要检查它们是否用到旧版 API(如 context.getScope 等)必要时要更新自定义规则以兼容 v9 API
测试 & lint 现有代码升级后先跑 eslint .,看报错 / 规则行为是否有变化,再逐步 fix有可能部分规则表现变严格或行为有微调,需要你改旧代码或调整规则
配置顺序 / 覆盖关系在 flat config 模式下,顺序比以前更重要:你需要在配置数组里按顺序组织 ignores / globals / 文件范围 / 插件 / 规则覆盖 / prettier 等若直接把旧的 extends 拷过来可能不起作用,或覆盖次序不对
编辑器 / IDE 支持确认编辑器插件(VSCode ESLint 插件等)是否支持 ESLint v9 / flat config有些编辑器插件版本可能只兼容旧格式,导致无法读取配置或不生效
日志 / 警告 / 弃用提示升级时注意查看 ESLint 在运行时输出的弃用警告 / 升级提示某些 API 将被弃用 / 警告语句会给出迁移建议

三、迁移到 v9 的建议步骤(高阶流程)

下面是一个建议的迁移流程:

  1. 把 eslint 升级到 v9,先保持现有 .eslintrc.* 格式

  2. 升级所有插件 / 共享配置 到兼容 v9 的版本

  3. 运行 lint, 修复报错,特别注意那些插件不兼容导致的错误

  4. (可选)迁移到 flat config

    • 新建 eslint.config.js(或 .ts
    • 把配置拆成数组,每个插件 / 规则作为单元插入
    • 使用插件提供的 flat config 接口(如果支持的话)
    • 删除旧的 .eslintrc.* 配置
  5. 测试工具链:CI、编辑器、lint-staged、webpack plugin 等都正常运行

  6. 逐步清理兼容层 / 适配代码,如 @eslint/compat、包装器等