Skip to content

eslint

eslint9

Configuration File

eslint.config.js eslint.config.mjs eslint.config.cjs eslint.config.ts (requires additional setup) eslint.config.mts (requires additional setup) eslint.config.cts (requires additional setup)

eslint Configure Language Options

使用的 JavaScript 语言特性和环境 它影响 ESLint 支持的语法版本、模块类型和全局变量定义

js
//eslint.config.js
// eslint.config.js
import { defineConfig } from "eslint/config";
export default defineConfig([
  {
    languageOptions: {
      ecmaVersion: 2020,      // 支持 ES2020 语法
      sourceType: "module",   // 代码作为 ES 模块处理
      parserOptions: {
        ecmaFeatures: {
          jsx: true           // 支持 JSX 语法
        }
      },
      globals: {
        // languageOptions.globals 是告诉 ESLint 语言解析层面有哪些全局变量,比如 window、process 等,
        // 重点是影响规则和解析器逻辑,避免对全局变量报未定义错误
        myGlobalVar: "readonly"
      }
    }
  },

]);

基本概念

  • 规则 rules
  • 配置文件
  • 可共享配置 extends
    • 指定一个或多个现成的规则集(配置文件)作为基础配置。plugins和rule的集成品
    • 以 eslint-config- 开头:eslint-config-myconfig
    • 如果是 npm 范围模块,则只需将模块以 @scope/eslint-config 命名或以此为前缀命名即可,如 @scope/eslint-config 和 @scope/eslint-config-myconfig。
    • 使用
    js
    {
      "extends": "eslint-config-myconfig"
      // or
      "extends": "myconfig"
      // or
      "extends": ["eslint:recommended", "plugin:react/recommended"]
    }
  • 插件 plugins
    • plugin 定义自己的规则(自己可以配置)
    js
    {
      plugins: [
        'eslint-plugin-react'
      ],
      rules: {
        'react/jsx-boolean-value': 2
      }
    }
  • 解析器
  • settings
    • 所有规则中共享的信息(全局)
    • 主要是给规则插件提供额外的配置信息或全局上下文
    js
    // eslint.config.js
    export default [
      {
        settings: {
          react: {
            version: "detect",
          },
          "import/resolver": {
            alias: {
              map: [["@", "./src"]],
              extensions: [".js", ".jsx", ".ts", ".tsx"],
            },
          },
        },
      },
    ];

基本操作

  • 给项目初始化eslint
bash
npm init @eslint/config@latest
# or
yarn create @eslint/config
# or
pnpm create @eslint/config@latest
bash
eslint ./src/**/*.{js,jsx} #对文件进行检查校验
eslint . --no-cache # 清除缓存
json
{
  "scripts": {
    "lint": "eslint ./src/**/*.{js,jsx}",
  },
}

安装

  • 现在pnpm create @eslint/config@latest执行以后
bash
 How would you like to use ESLint? · problems
 What type of modules does your project use? · esm
 Which framework does your project use? · react
 Does your project use TypeScript? · javascript
 Where does your code run? · browser
The config that you've selected requires the following dependencies:
eslint, globals, @eslint/js, eslint-plugin-react
√ Would you like to install them now? · No / Yes
√ Which package manager do you want to use? · pnpm
  • 默认会安装"eslint": "^9.15.0",v9已经使用新的配置系统:
  • 平面配置文件
  • 1、文件 .eslintrc.js/.eslintrc.json -> eslint.config.js
js
import globals from "globals";
import pluginJs from "@eslint/js";
import pluginReact from "eslint-plugin-react";
/** @type {import('eslint').Linter.Config[]} */
export default [
  {files: ["**/*.{js,mjs,cjs,jsx}"]},
  {languageOptions: { globals: globals.browser }},
  pluginJs.configs.recommended,
  pluginReact.configs.flat.recommended,
];
  • 2、原来的配置会有兼容的问题
  • plugins
    js
    // .eslintrc.js
    module.exports = {
        // ...other config
        plugins: ["jsdoc"],
        rules: {
            "jsdoc/require-description": "error",
            "jsdoc/check-values": "error"
        }
        // ...other config
    };
    js
    // eslint.config.js
    import jsdoc from "eslint-plugin-jsdoc";
    export default [
        {
            files: ["**/*.js"],
            plugins: {
                jsdoc: jsdoc
            },
            rules: {
                "jsdoc/require-description": "error",
                "jsdoc/check-values": "error"
            }
        }
    ];
  • 2、可共享配置
    • 你可能会发现有一个你依赖的可共享配置尚未更新为平面配置格式。
    • 在这种情况下,你可以使用 FlatCompat 工具将 eslintrc 格式转换为平面配置格式。
    • 首先,安装 @eslint/eslintrc 包:npm install @eslint/eslintrc --save-dev
    js
    import { FlatCompat } from "@eslint/eslintrc";
    import path from "path";
    import { fileURLToPath } from "url";
    // mimic CommonJS variables -- not needed if using CommonJS
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    const compat = new FlatCompat({
        baseDirectory: __dirname
    });
    export default [
        // mimic ESLintRC-style extends
        ...compat.extends("eslint-config-my-config"),
    ];
    js
    /**
    解决eslint-plugin-react-hooks加载问题,
    eslint-plugin-react-hooks是react官网的eslint插件,并没有实现平面配置格式
    https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks
    {
      "extends": [
        // ...
        "plugin:react-hooks/recommended"
      ]
    }
    */
    import globals from "globals";
    import pluginJs from "@eslint/js";
    import pluginReact from "eslint-plugin-react"; //https://github.com/jsx-eslint/eslint-plugin-react#configuration
    import { FlatCompat } from "@eslint/eslintrc";
    import path from "path";
    import { fileURLToPath } from "url";
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = path.dirname(__filename);
    const compat = new FlatCompat({
        baseDirectory: __dirname
    });
    
    /** @type {import('eslint').Linter.Config[]} */
    export default [
      {
        files: ["**/*.{js,mjs,cjs,jsx}"],
        rules: {
          "no-unused-vars": ["error", {"args": "none",}], // int.org/docs/latest/rules/no-unused-vars
          // 'no-param-reassign': ['error', { props: true, ignorePropertyModificationsFor: ['evt'] }],
          'react/prop-types': ['error', { skipUndeclared: true }],
          'react/no-unknown-property': ['error', { ignore: ['css'] }],
        },
      },
      ...compat.extends("plugin:react-hooks/recommended"),
      {languageOptions: { globals: globals.browser }},
      pluginJs.configs.recommended,
      pluginReact.configs.flat.recommended,
    ];
  • 参考:

rules

js
module.exports = {
  root: true,
  extends: [''],
  settings: {
    'import/resolver': {
      'custom-alias': {
        alias: {
          '@': './src',
        },
      },
    },
  },
  rules: {
    'react/prop-types': ['error', { skipUndeclared: true }],
    'react/no-unknown-property': ['error', { ignore: ['css'] }],
    "no-unused-vars": ["error", {"args": "none",}], // int.org/docs/latest/rules/no-unused-vars
    // https://github.com/import-js/eslint-plugin-import/blob/v2.31.0/docs/rules/no-unresolved.md
    "import/no-unresolved": [0] // import tailwindcss from '@tailwindcss/vite' 报错
    "import/no-unresolved": [2, { caseSensitive: true }] // 严格 区分大小写
  },
}

eslint 自定义插件

自定义插件eslint-plugin-lodash.js

js
const plugin = {
  meta: {
    name: 'eslint-plugin-lodash',
    version: '1.0.0',
  },
  configs: {},
  rules: {
    'scope-test':{
      meta: {
        type: 'problem',
        docs: {
          description: `
            根据name不是"_"节点,获取当前节点 node,
            再根据node.parent 获取其作用域,有要求:node 是 function(普通变量获取不到)等,sourceCode.scopeManager.acquire(node.parent)才可以获取作用域
            获取作用域中变量,是否有变量名为"_",没有则报错`,
          category: 'Best Practices',
          recommended: true,
        },
        schema: [], // no options
      },
      create(context){
        let globalScope, globalVariables;
        const sourceCode = context.getSourceCode()
        return {
          Identifier(node) { // 标识符表达式(变量名、函数名等)
            // if (node.name !== '_') return
            // sourceCode.scopeManager.acquire(...) 是 ESLint 内部用于获取当前 AST 节点作用域的方法。
            // Identifier 等普通节点(变量名、属性名)不是作用域创建节点,因此传给 acquire() 会返回 null。
            const scope = sourceCode.scopeManager.acquire(node.parent) // 是当前标识符的父节点,目的是获取标识符所在的实际作用域
            // console.log("scope",scope)
            if (!scope) {
              // 没作用域,无法判断,安全起见不报错
              return;
            }
            // 查找当前作用域中是否声明了名为 _ 的变量
            const variable = scope.variables.find((v) => v.name === '_')
            // console.log("variable",variable)
            // 沒有声明"_",那反过来判断是全局有了,就提示,一种很不严谨的处理,仅当熟悉插件api写法,与我想写的插件本意相去甚远
            if (!variable || variable.defs.length === 0) {
              context.report({
                node,
                message: 'Usage of global lodash `_` is forbidden. Please import lodash methods explicitly.',
              })
            }
          },
          Program(node) {
            // 分析全局作用域变量	Program 节点	程序入口,只调用一次,效率最高
            globalScope = context.getSourceCode().scopeManager.globalScope;
            globalVariables = new Set(globalScope.variables.map(v => v.name));
            console.log(globalVariables)
            // 在这里分析顶层变量,进行标识符等批量或者条件过滤逻辑
          }
        }
      },

    },
    'no-global-import-lodash': {
      meta: {
        type: 'problem',
        docs: {
          description: 'Disallow global import of entire lodash',
          category: 'Best Practices',
          recommended: true,
        },
        schema: [], // no options
        messages: {
          noGlobalLodashImport: "禁止从 'lodash' 导入整个库,请按需导入。"
        }
      },
      create(context) {
        return {
          ImportDeclaration(node) {
            // 判断导入源是否是 'lodash'
            if (node.source.value === "lodash") {
              // 检查是否是默认导入(import _ from 'lodash')
              const hasDefaultImport = node.specifiers.some(
                specifier => specifier.type === "ImportDefaultSpecifier"
              );
              if (hasDefaultImport) {
                // 报告错误提醒
                context.report({
                  node,
                  messageId: "noGlobalLodashImport"
                });
              }
            }
          }
        }
      },
    },
    // 'no-import-global-lodash': {
    //   meta: {
    //     type: 'problem',
    //     docs: {
    //       description: 'Disallow usage of global lodash `_` identifier',
    //       category: 'Best Practices',
    //       recommended: true,
    //     },
    //     schema: [], // no options
    //   },
    // }
  },
  processors: {},
}

// for ESM
export default plugin

// OR for CommonJS
// module.exports = plugin;

自定义插件的 eslint.config.js

js
// eslint.config.js
import { defineConfig } from "eslint/config";
import lodash from "./eslint-plugin-lodash.js";

export default defineConfig([
	{
		files: ["src/**/*.js"],
		/*
			ignorePatterns 旧版 eslintrc 配置里使用的
			v9.27.0
			ignorePatterns 弃用
			.eslintignore 也不被支持了( ESLintIgnoreWarning: The ".eslintignore" file is no longer supported.)
		*/
		ignores: ["eslint-plugin-*.js"],// ignores配置
		// 参考:https://eslint.org/docs/latest/use/configure/configuration-files#specifying-files-and-ignores
		// 参考:https://eslint.org/docs/latest/use/configure/ignore
		plugins: {
			lodash,
		},
		rules: {
			"lodash/scope-test": "off",
			"lodash/no-global-import-lodash": "error",
		},
	},
]);

eslint-plugin-no-global-lodash 插件实现参考

js
/**
 * @fileoverview Lodash should not be loaded globally. If a lodash method is needed only the package for that method should be loaded in the file
 * @author Adalberto Teixeira
 */
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

module.exports = {
  meta: {
    docs: {
        description: "Lodash should not be loaded globally. If a lodash method is needed only the package for that method should be loaded in the file",
        category: "Fill me in",
        recommended: false
    },
    fixable: 'code',
    schema: []
  },

  create: function(context) {

    //----------------------------------------------------------------------
    // Helpers
    //----------------------------------------------------------------------

    const getLocation = (node, defs) => {
      const loc = {
        line: node.loc.end.line,
        column: node.loc.start.column,
      }
      if (!defs || !defs.length) return loc;
      loc.line = defs[0].name.loc.start.line;
      loc.column = defs[0].name.loc.start.column;
      return loc;
    };

    const getErrorMessage = (declaredVariableName) => {
      if (!declaredVariableName || declaredVariableName === '_') {
        return 'Expected global "lodash" not to be imported.';
      }

      return `Expected ${declaredVariableName} to be imported as "import ${declaredVariableName} from 'lodash.${declaredVariableName.toLowerCase()}'".`
    };
    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------

    return {
      ImportDeclaration: function (node) {
        if (node.source.value === 'lodash') {
          const declaredVariables = context.getDeclaredVariables(node);
          if (declaredVariables && declaredVariables.length) {
            declaredVariables.forEach(declaredVariable => {
              const loc = getLocation(node, declaredVariable.defs);
              const errorMessage = getErrorMessage(declaredVariable.name);
              context.report({
                node,
                loc,
                message: errorMessage,
              })
            })
          }
        }
      }
    };
  }
};

自定义 processor 实现

SLint 自定义插件中的 processors 处理器主要用于处理非标准 JavaScript 文件,例如 .vue、.html、.md 等, 或者在 ESLint 解析 JavaScript 代码之前对代码进行预处理。 它允许您从其他类型的文件中提取 JavaScript 代码,然后让 ESLint 对提取出的 JavaScript 代码进行 lint 检查。

processors 处理器包含两个主要方法:preprocess 和 postprocess。

  • preprocess(text, filename):

    • 接收文件内容和文件名作为参数,返回一个代码块数组。
    • 每个代码块都包含 text(代码内容)和 filename(文件名)两个属性。
    • filename 属性可以是任意值,但应包含文件扩展名,以便 ESLint 知道如何处理代码块。
    • ESLint 会分别检查每个代码块,但它们仍然与原始文件名关联。
  • postprocess(messages, filename):

    • 接收一个二维数组 messages 和文件名作为参数。
    • messages 数组中的每个元素对应于 preprocess 方法返回的代码块中的 lint 信息。
    • postprocess 方法必须调整所有错误的位置,使其与原始的、未处理的代码中的位置对应,并将它们聚合成一个一维数组并返回。
js
module.exports = {
  processors: {
    ".md": {
      preprocess: (text, filename) => {
        return text.split("```javascript")
          .slice(1) // 去除第一个元素(Markdown 文件 JavaScript 代码块之前的部分)
          .map((section) => {
            const code = section.split("```")[0]; // 提取 JavaScript 代码
            return { text: code, filename: "temp.js" };
          });
      },
      postprocess: (messages, filename) => {
        return messages.flat(); // 将二维数组扁平化成一维数组
      },
    },
  },
};
json
{
  "plugins": ["./processor"], // 假设 processor.js 与配置文件在同一目录
  "overrides": [
    {
      "files": ["*.md"],
      "processor": ".md"
    }
  ]
}

自定义 configs 实现

ESLint 自定义插件中的 configs 主要用于定义一组规则配置的集合,方便用户通过配置扩展直接引用这组预设规则,而不必一条一条单独启用

js
const plugin = {
	meta: {
		name: "eslint-plugin-example",
		version: "1.2.3",
	},
	configs: {},
	rules: {
		"dollar-sign": {
			create(context) {
				// rule implementation ...
			},
		},
	},
};
// assign configs here so we can reference `plugin`
Object.assign(plugin.configs, {
	recommended: [
		{
			plugins: {
				example: plugin,
			},
			rules: {
				"example/dollar-sign": "error",
			},
			languageOptions: {
				globals: {
					myGlobal: "readonly",
				},
				parserOptions: {
					ecmaFeatures: {
						jsx: true,
					},
				},
			},
		},
	],
});
// for ESM
export default plugin;
// OR for CommonJS
module.exports = plugin;

參考:

官方指导

参考

根据 ESLint 的作用域分析规则,常见的作用域创建节点包括:

节点类型说明
Program整个脚本或模块的顶层作用域
FunctionDeclaration函数声明,创建函数作用域
FunctionExpression函数字面量,创建函数作用域
ArrowFunctionExpression箭头函数,创建函数作用域
ClassDeclaration类声明,创建类的块级作用域
ClassExpression类表达式,同上
CatchClausetry-catch 的 catch 子句,创建作用域
BlockStatementES6 及之后的块级作用域(let/const 所在块)
SwitchStatementswitch 语句,创建块级作用域
ForStatementfor 循环语句,创建块级作用域
ForInStatementfor..in 循环,创建块级作用域
ForOfStatementfor..of 循环,创建块级作用域