ESlint - 盘它

929 阅读9分钟

首先 有一个包叫做eslint, 你所有的一切lint校验都来自于这个包 - 官方文档

涉及到的前置知识点

1. AST - 抽象语法树

AST 是 Abstract Syntax Tree 的简称, 中文名: 抽象语法树

AST的作用: 将代码抽象成树状的数据结构,方便后续的分析和检测

ps: 此处产生疑问,为何要转成的树状结构?是因为转树状结构成本最低还是因为它最适合分析和检测,如果是方便分析和检测,那优势在哪里?

代码解析成AST的样子

astexplorer.net 是一个工具网站,可以查看代码被转成AST的样子

​ 如下图所示,此时左侧选中的部分(printTips),右侧实时高亮显示

AST_demo.png

AST选择器

下图中被圈起来的部分,称为AST selectors(选择器)。

作用:使用代码通过选择器来选中特定的代码片段,然后再对代码进行静态分析。

AST的选择器很多,ESLint官方专门列出了所有类型的选择器: estree

了解的过程中会用到选择器,现在了解一下即可。

AST.png

ESLint的运行原理

1.将代码解析成AST

Eslint 默认使用Javascript解析器espree将JS代码解析成AST

PS: 解析器: 将代码解析成AST的工具,ES6 vue react都开发了对应的解析器, 所以eslint可以解析它们,eslint也因此一统前端。

这也是eslint配置文件中要配置的parser选项, 由于现在babel在前端项目中的不可或缺,因此常用的基础解析器是: babel-eslint。

2. 深度遍历AST,监听匹配过程

在经过第一步之后,ESlint会拿到AST的结果,然后以‘从上至下’再‘从下至上’的顺序遍历每个选择器2次。

3. 触发监听选择器的rule回调

在深度遍历的过程中,生效的每条规则都会对其中的某一个或多个选择器进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。

PS: 一段代码解析后可能包含多次同一个选择器,选择器的钩子也会多次触发

4.具体的检测细节 。。。

##Eslint使用

eslint的使用包括2部分,* 通过配置文件配置 lint 规则;* *通过命令行执行 lint,找出不符合规范的地方(当然有些不符合的规则也可以尝试修复)

配合编辑器插件,ESLint 也能很好的起作用,实际上,很多人可能更习惯这种用法。

  1. 使用 eslint --init 根据提示生成.eslintrc.js 配置文件
  2. 使用eslint --lint执行检查

配置文件

  1. eslint配置文件 .eslintrc.js 举例说明:

    • env - 预先定义那些环境中需要用到的环境变量,可用参数 es6 / node / browser 等

      browser -- 会添加所有浏览器的环境变量,比如window

      node -- 会添加所有nodeJS运行时的环境变量,比如global process等

      es6 -- 启用除了 modules 以外的所有 ECMAScript 6 特性(该选项会自动设置 ecmaVersion 解析器选项为 6)

      commonjs - CommonJS 全局变量和 CommonJS 作用域 (用于 Browserify/WebPack 打包的只在浏览器中运行的代码

      官方文档参考

    • extends - 指定扩展的配置,配置支持递归扩展,支持规则的覆盖和聚合。

      可选属性值:

      1. 指定配置的字符串 - (配置文件的路径、可共享配置的名称(airbnb)、eslint:recommendedeslint:all)

      2. 字符串数组 - 数组每一项的配置继承写在前边的配置 - 例如

        extends: ['eslint:recommended`', 'eslint:all'], // eslint:all的配置继承eslint:recommended的配置

    eslint:recommended - 启用eslint的核心规则, 这些规则在规则页面被标记为✅, 跟随eslint主版本进行更新。

    eslint:all - 启动当前安装的eslint所有的核心规则 - 不推荐在项目中使用, 因为不管是大版本还是小版本,这些规则都可能被修改。

    airbnb - 社区据说用的最好的代码lint规则

    官方链接传送门

    • plugins - 这里配置我们需要的lint插件

    • parser - parser选项- 解析器,即告诉eslint你要使用哪个解析器进行解析代码,它支持的选项常用的以下几种:

    Espree - 这个是eslint默认使用的解析器 - 但是一旦我们使用babel的话,我们需要用babel-eslint

babel-eslint - 这个依赖包允许你使用一些实验性的特性的时候,依然可以使用eslint的语法检查。 主要指es6, es7 等等的.

@typescript-eslint/parser - typescript语法的解析器,tslint已经弃用,官方不再维护,不必关注。这个主要会支持一些typescript的语法,比如类型断言 const a! : number 或者在使用的时候会有 a as number 这种情况。 ​

  • parserOptions - 在parser配置为babel-eslint的时候,必须配置这个选项,可选值 (。

    ecmaVersion - 默认5, 可指定3、5、6、7、8、9、10,用来指定使用哪一个ECMAScript版本的语法。也可以设置基于年份的JS标准,比如2015(ECMA 6)

    sourceType - 如果你的代码是ECMAScript 模块写的,该字段配置为module,否则为script(默认值)

    ecmaFeatures - 该对象指示你想使用的额外的语言特性

    globalReturn:允许全局范围内的return语句

    impliedStrict:使能全局strict模式

    jsx:使能JSX

    • rules - 自定义规则配置 --优先级最高,可以覆盖掉我们在extends中配置启用的规则
    • settings - 该字段定义的数据可以在所有的插件中共享。这样每条规则执行的时候都可以访问这里面定义的数据
module.exports = {
  env: 'es6',
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module'
  },
  extends: ['eslint:recommended`', 'eslint:all'], // eslint:all的配置继承eslint:recommended的配置
  rules: {
    'no-unused-vars': [
      'error',
      // we are only using this rule to check for unused arguments since TS
      // catches unused variables but not args.
      { varsIgnorePattern: '.*', args: 'after-used', argsIgnorePattern: '^_' }
    ],
    // most of the codebase are expected to be env agnostic
    'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals],
    // since we target ES2015 for baseline support, we need to forbid object
    // rest spread usage (both assign and destructure)
    'no-restricted-syntax': [
      'error',
      'ObjectExpression > SpreadElement',
      'ObjectPattern > RestElement'
    ]
  },
  settings: {
    'import/resolver': { // This config is used by eslint-import-resolver-webpack
      webpack: {
        config: './webpack/webpack-common-config.js'
      }
    },
  },
  overrides: [
    // tests, no restrictions (runs in Node / jest with jsdom)
    {
      files: ['**/__tests__/**', 'test-dts/**'],
      rules: {
        'no-restricted-globals': 'off',
        'no-restricted-syntax': 'off'
      }
    },
    // shared, may be used in any env
    {
      files: ['packages/shared/**'],
      rules: {
        'no-restricted-globals': 'off'
      }
    },
    // Packages targeting DOM
    {
      files: ['packages/{vue,runtime-dom}/**'],
      rules: {
        'no-restricted-globals': ['error', ...NodeGlobals]
      }
    },
    // Packages targeting Node
    {
      files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
      rules: {
        'no-restricted-globals': ['error', ...DOMGlobals],
        'no-restricted-syntax': 'off'
      }
    },
    // Private package, browser only + no syntax restrictions
    {
      files: ['packages/template-explorer/**'],
      rules: {
        'no-restricted-globals': ['error', ...NodeGlobals],
        'no-restricted-syntax': 'off'
      }
    }
  ]
}

个人常用配置项扩展说明:

eslint 配置文件常用的配置选项注意:

  1. parser- 首先需要配置解析器,这个在目前的前端环境下是必须的,如果不配置就是采用默认的javascript解析器 - espree,我们一般使用babel-eslint。 有时候解析选项也是需要进行配置的parserOptions

  2. env - 指定脚本运行的运行时环境,可以同时指定多个,具体见官方文档

  3. extends - 指定我们启用哪一套eslint规则,一般启用标准规则, 这里可以写多个规则启用项目,后边的覆盖前边的,也可引入我们的自定义规则(既如此,我们就可以把我们习惯的规则引入,或者把我们自定义的插件配置在此处)

  4. plugins - 这个地方配置第三方的插件,插件需要安装对应的npm包, 可以写多个,形式为字符串数组, 前缀eslint-plugin-可省略

  5. rules - 此处启用的规则优先级最高,可以配置eslint的自有规则,也可配置从第三方插件中引入的规则(此处需要保证第三方插件的npm包已经安装).

  6. overrides & files - 针对项目中特定的部分文件禁用eslint规则,可进行如下配置:

    overrides: [
     // tests, no restrictions (runs in Node / jest with jsdom)
     {
       files: ['**/__tests__/**', 'test-dts/**'],
       rules: {
         'no-restricted-globals': 'off',
         'no-restricted-syntax': 'off'
       }
     },
     // shared, may be used in any env
     {
       files: ['packages/shared/**'],
       rules: {
         'no-restricted-globals': 'off'
       }
     },
     // Packages targeting DOM
     {
       files: ['packages/{vue,runtime-dom}/**'],
       rules: {
         'no-restricted-globals': ['error', ...NodeGlobals]
       }
     },
     // Packages targeting Node
     {
       files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
       rules: {
         'no-restricted-globals': ['error', ...DOMGlobals],
         'no-restricted-syntax': 'off'
       }
     },
     // Private package, browser only + no syntax restrictions
     {
       files: ['packages/template-explorer/**'],
       rules: {
         'no-restricted-globals': ['error', ...NodeGlobals],
         'no-restricted-syntax': 'off'
       }
     }
     ]
    

编写一个Eslint插件

​ 既然要写一个插件,必要明确插件要实现的功能目标,eslint插件可实现一个校验的规则。

#### 插件要实现的目标
  1. 禁止setTimout的第二个参数是数字。
  2. 进阶 - 规则增强 - 项目中禁止出现魔幻变量。

开发准备工作

  1. eslint官方为了方便开发者开发插件,提供了Yeoman模版(generator-eslint), 它是一个脚手架工具,用于生成包含指定框架结构的工程化目录结构。

    npm install -g yo generator-eslint
    
  2. 创建一个文件夹,用来存放你的项目

    mkdir -p eslint-plugin-demo && cd eslint-plugin-demo
    
  3. 初始化eslint插件的目录结构

    yo eslint:plugin
    

    初始化eslint规则的模版文件

       yo eslint:rule
    

    生成之后的文件目录结构

     ├── README.md
     ├── docs // 使用文档
     │   └── rules // 所有规则的文档
     │       └── settimeout-no-number.md // 具体规则文档
     ├── lib // eslint 规则开发
     │   ├── index.js 引入+导出rules文件夹的规则
     │   └── rules // 此目录下可以构建多个规则
     │       └── settimeout-no-number.js // 规则细节
     ├── package.json
     └── tests // 单元测试
         └── lib
             └── rules
                 └── settimeout-no-number.js // 测试该规则的文件
    

安装依赖

npm install

开始开发

​ 有2个文件需要我们关注:

​ 第一个 lib/rules/settimeout-no-number.js, 这个是规则模板文件,也是我们开发自定义规则的文件: (初始大致这个样子)

module.exports = {
 meta: {
     docs: {
         description: "setTimeout 第二个参数禁止是数字",
     },
     fixable: null,  // 修复函数
 },
// rule 核心
 create: function(context) {
    // 公共变量和函数应该在此定义
     return {
         // 返回事件钩子
     };
 }
};

完整代码:

/**
 * @fileoverview The second parameter of setTimeout is forbidden to be a number
 * @author Arche
 */
"use strict";

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

module.exports = {
    meta: {
        docs: {
            description: "The second parameter of setTimeout is forbidden to be a number",
            category: "Fill me in",
            recommended: false
        },
        fixable: 'code',  // or "code" or "whitespace" // 是否开启文件修复函数
        schema: [
            // fill in your schema
        ]
    },

    create: function(context) {

        // variables should be defined here

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

        // any helper functions should go here or else delete this section

        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------

        return {
          'CallExpression': (node) => {
            if (node.callee.name !== 'setTimeout') return //检测是否是定时器,不是return
            const parameterTime = node?.arguments[1]; // 获取第二个参数
            if (!parameterTime) return // 如果第二个参数不存在则return
            if(parameterTime.type === 'Literal' && typeof parameterTime.value === 'number') {
              context.report({
                node,
                message: 'The second parameter is forbidden to be a number', // 这个提示信息需要和测试用例中的保持完全一致
                fix: (fixer) => { // 这个是文件修复函数
                  const numberValue = parameterTime.value;
                  const statementString = `const countNumber = ${numberValue}\n`; // 修复语句
                  return [
                    fixer.replaceTextRange(node.arguments[1], numberValue), // 替换第二个参数位置
                    fixer.insertTextBeforeRange(node.range, statementString) // 在定时器前增加一条语句
                  ]
                }
              })
            }
          }
        };
    }
};

第二个 tests/lib/rules/settimeout-no-number.js 这个是测试用例文件。

/**
* @fileoverview The seconed parameter of setTimeout is forbidden to be a
* @author Arche
*/
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

var rule = require("../../../lib/rules/setTimeout-no-number"),

   RuleTester = require("eslint").RuleTester;


//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

var ruleTester = new RuleTester({
 parserOptions: {
   ecmaVersion: 7, // 默认支持语法为es5
 },
});
ruleTester.run("setTimeout-no-number", rule, {

   valid: [
     {
       code: "let num = 1000; setTimeout(() => { console.log(1) }, num)"
     },
     {
       code: 'setTimeout(()=>{ console.log(11) },someNumber)'
     }
   ],

   invalid: [
       {
           code: "setTimeout(() => {}, 1000)",
           errors: [{
               message: "The second parameter is forbidden to be a number",
               type: "CallExpression"
           }]
       }
   ]
});

参考资料:

手摸手教你写个eslint插件以及了解eslint的运行原理

最全的Eslint配置模板,从此统一团队的编程习惯

官方文档