pfan123/Articles

自定义 Eslint 开发

pfan123 opened this issue · 0 comments

对于前端开发者来说,ESLint 是比较常用的代码规范和错误检查工具,ESLint 非常强大不仅提供了大量的常用 rules,还有许多实用的 ESLint 插件可以满足各样需求。但随着项目不断迭代发展,可能会遇到已有 ESLint 插件不能满足现在团队开发的情况。那么这时候,我们就需要自定义 Eslint 开发 ESLint Shareable ConfigESLint Plugins

ESLint Shareable Config 开发

可分享的扩展配置(eslint-config-<config-name> 是一个 ESLint 配置对象 npm 包,模块名称以 eslint-config-<config-name>@<scope>/eslint-config-<config-name> 命名,创建比较简单导出配置规则即可。

创建扩展配置

创建扩展配置非常简单,创建一个新的 index.js 文件并 export 一个包含配置的对象即可:

module.exports = {
    globals: {
        MyGlobal: true
    },
    rules: {
        semi: [2, "always"]
    }
}

更多配置字段,参考 Configuring ESLint

使用扩展配置

npm 发布扩展包,引入 ESLint 配置:

module.exports = {
// extends: ['antife', 'myconfig'],
  extends: ['eslint-config-antife', 'eslint-config-myconfig'],
  globals: {
    'EVENT': true,
    'PAGE': true,
    'SCENE': true,
    'AlipayJSBridge': true,
  },
  plugins: [
    'babel',
    // 'html',  // eslint-plugin-html 从 <script> 标记中提取内容,eslint-plugin-vue 需要 <script> 标记和<template> 标记,两者同时存在会冲突
    'vue',
  ]
}

Eslint plugin 开发

插件(eslint-plugin-<plugin-name> 是一个命名格式为 eslint-plugin-<plugin-name> 的 npm 包,模块名称以 eslint-plugin-<plugin-name>@<scope>/eslint-plugin-<plugin-name> 命名。

Eslint plugin 目录

我们可以利用 yeomangenerator-eslint 来构建插件的目录结构进行开发,这里我们选用自定义目录,如下:

├── README.md
├── _tests__
├── docs
├── index.js
└── rules
    └── my-rule.js

插件主入口组成部分

  • Rules - 插件必须输出一个 rules对象,包含规则 ID 和对应规则的一个键值对。

  • Environments - 插件可以暴露额外的环境以在 ESLint 中使用。

  • Processors - 定义插件如何处理校验的文件。

  • Configs - 可以通过配置指定插件打包、编译方式,还可提供多种风格校验配置。

module.exports = {
    rules: {
        "my-rules": {
            create: function (context) {
                // rule implementation ...
            }
        }
    },
    env: {
        jquery: {
            globals: {
                $: false
            }
        }
    },
    configs: {
        myConfig: {
            parser: require.resolve('vue-eslint-parser'),
            parserOptions: {
                ecmaVersion: 2018,
                sourceType: 'module',
                ecmaFeatures: {
                    jsx: true
                }
            },
            plugins: ["myPlugin"],
            env: ["browser"],
            rules: {
                "myPlugin/my-rule": "error",
            }
        },
        myOtherConfig: {
            plugins: ["myPlugin"],
            env: ["node"],
            rules: {
                "myPlugin/my-rule": "off",
            }
        }
    },
    processors: {
        '.vue': {
            // takes text of the file and filename
            preprocess: function(text, filename) {
                // here, you can strip out any non-JS content
                // and split into multiple strings to lint

                return [string];  // return an array of strings to lint
            },

            // takes a Message[][] and filename
            postprocess: function(messages, filename) {
                // `messages` argument contains two-dimensional array of Message objects
                // where each top-level array item contains array of lint messages related
                // to the text that was returned in array from preprocess() method

                // you need to return a one-dimensional array of the messages you want to keep
                return messages[0];
            },

            supportsAutofix: true // (optional, defaults to false)
        }
    }
}

Rules 创建

在开始编写新规则之前,请阅读官方的 ESLint指南,了解下 ESLint 的特点:

  • ESLint 使用 Espree 进行JavaScript解析。
  • ESLint 使用 AST 评估校验代码。
  • ESLint 是完全可插入的,每个规则都可以是一个插件。
  • ESLint 每条规则相互独立,可以设置禁用off、警告warn⚠️和报错error❌,当然还有正常通过不用给任何提示。

我们可以通过使用 astexplorer.net, 去了解 ESLint 如何使用 AST 评估校验代码,astexplorer.net 非常强大,还支持 Vue 模板。

规则组成部分

  • meta 对象包含规则的元数据

    • type 属性表示规则的类型,这是一个"problem""suggestion""layout"
    • docs 属性是ESLint的核心规则所必需的描述类信息
    • fixable 属性是"code""whitespace" ,如果规则不可修复,请省略fixable属性
    • schema 指定 options 以便ESLint可以防止无效的 规则配置
    • deprecated 属性指示规则是否已被弃用
    • replacedBy 属性表示在不建议使用的规则的情况下,指定替换规则
  • create 函数返回 ESLint 调用方法对象,通过该方法访问 JavaScript 代码的抽象语法树(由ESTree定义的AST)节点

  • context 对象包含与规则上下文相关的信息

    • 属性:

      • parserOptions - 插件配置的解析器选项

      • id - 规则ID。

      • options - 规则的 配置选项

      • settings- 配置中的 共享设置

      • parserPath parser - from配置的名称

      • parserServices - 包含解析器为规则提供的服务的对象

    • 方法:

      • getAncestors() - 返回当前遍历的节点的祖先数组,从AST的根部开始,一直到当前节点的直接父级

      • getCwd() - 将 cwd 传递的内容返回给Linter, 为当前工作目录

      • getDeclaredVariables - 返回给定节点声明的

      • getFilename() - 返回与源关联的文件名

      • getScope() - 返回当前遍历的节点的 scope ,用于跟踪对变量的引用

      • getSourceCode() - 返回一个 SourceCode 对象,可以使用该对象来处理传递给ESLint的源

      • markVariableAsUsed(name) - 在当前作用域中使用给定名称标记变量

      • report(descriptor) - 报告代码中的问题

"use strict";

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

module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "disallow unnecessary semicolons",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-extra-semi"
        },
        fixable: "code",
        schema: [] // no options
    },
    create: function(context) {
        context.getScope()
        context.report()
        return {
          Identifier(node) {
              if (node.name === "foo") {
                  context.report({
                      node,
                      messageId: "avoidName",
                      data: {
                          name: "foo",
                      }
                  })
              }
            },
            ExportDefaultDeclaration(node){
              context.report({
                node,
                message: "test",
              })
            }
        }
    }
}

若我们需要校验 Vue 模板,这里要注意由于 Vue 中的单个文件组件不是普通的 JavaScript,因此无法使用默认解析器,因此引入了新的解析器 vue-eslint-parser

要了解更多 vue AST 知识,可以查看

自定义 Processors

ESLint 插件开发,支持自定义处理器来处理 JavaScript 之外的文件,自定义处理器含有两个过程:preprocesspostprocess。自定义处理器大体结构如下:

module.exports = {
    processors: {

        // assign to the file extension you want (.js, .jsx, .html, etc.)
        ".ext": {
            // takes text of the file and filename
            preprocess: function(text, filename) {
                // here, you can strip out any non-JS content
                // and split into multiple strings to lint

                return [string];  // return an array of strings to lint
            },

            // takes a Message[][] and filename
            postprocess: function(messages, filename) {
                // `messages` argument contains two-dimensional array of Message objects
                // where each top-level array item contains array of lint messages related
                // to the text that was returned in array from preprocess() method

                // you need to return a one-dimensional array of the messages you want to keep
                return messages[0];
            },

            supportsAutofix: true // (optional, defaults to false)
        }
    }
};

插件测试

ESLint 提供了 RuleTester 实用工具可以轻松地测试你插件中的规则,在 peerDependency 指向 ESLint 0.8.0 或之后的版本。

{
    "peerDependencies": {
        "eslint": ">=0.8.0"
    }
}

peerDependencies 目的是提示宿主环境去安装满足插件peerDependencies所指定依赖的包,然后在插件import或者require所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题。

// in the file to lint:

var foo = 2;
//  ^ error: Avoid using variables named 'foo'

// In your tests:
var rule = require("../rules/no-avoid-name")
var RuleTester = require("eslint").RuleTester

var ruleTester = new RuleTester()
ruleTester.run("no-avoid-name", rule, {
  valid: ["bar", "baz"],  // right data
  invalid: [  // error data
    {
      code: "foo",
      errors: [
          {
            messageId: "avoidName"
          }
      ]
    }
  ]
})

实践开发 Vue 模板 Eslint plugin

在开发之前,这里要注意由于 Vue 中的单个文件组件并不是普通的 JavaScript,导致无法使用默认解析器,因此引入了新的解析器 vue-eslint-parser

{
    "parser": "vue-eslint-parser",
    "parserOptions": {
        "parser": "babel-eslint",
        "sourceType": "module",
        "allowImportExportEverywhere": false
    }
}

开发 vue eslint 规则

这里要注意,涉及到自定义的解析器的,需要使用context.parserServices 访问该解析器解析的抽象语法树内容。vue-eslint-parser 提供了以下三个处理api 方法:

// 处理 template 语法树遍历方法
// Define handlers to traverse the template body  
context.parserServices.defineTemplateBodyVisitor()
// 获取缓存的 template 语法树结构
context.parserServices.getTemplateBodyTokenStore()
// 获取根结点 document fragment.
context.parserServices.getDocumentFragment()
  • Vue 插件规则,示例
module.exports = {
    meta: {
      docs: {
        description: 'disallow unnecessary `v-bind` directives',
        url: 'https://eslint.vuejs.org/rules/no-useless-v-bind.html'
      },
      fixable: 'code',
      type: 'suggestion'
    },

    create(context) {
      if (context.parserServices.defineTemplateBodyVisitor == null) {
        context.report({
          loc: { line: 1, column: 0 },
          message:
            'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error'
        })
        return {}
      }
  
      return context.parserServices.defineTemplateBodyVisitor({
        VElement(node){
          if(node.name === "template"){
            context.report({
              node,
              message: "template标签",
            })
          }
        },
  
        Identifier(node){
          console.error('Identifier.name', node.name)
        }
  
      })
    }
  }

若是校验 js,无需通过 context.parserServices.defineTemplateBodyVisitor 获取语法树信息

  • Vue 插件入口文件示例:
module.exports = {
    configs: {
      base: {
        parser: require.resolve('vue-eslint-parser'),
        plugins: ['boilerplate'],
        rules: {
          'boilerplate/no-avoid-name': 'error',
          'boilerplate/no-useless-v-bind': 'error'
        }
      }
    },
    env: {
      browser: true,
      es6: true
    },
    rules: {
      'no-avoid-name': require('./rules/no-avoid-name'),
      'no-useless-v-bind': require('./rules/no-useless-v-bind'),
    }
  }
  • 配置使用
module.exports = {
    parser: 'vue-eslint-parser',
    parserOptions: {
      parser: 'babel-eslint',
        ecmaVersion: 2018,
        sourceType: 'module'
    },
    // vue 插件要放在 extend 前面,防止出现覆盖,"eslint:recommended" 是默认推荐的规则
    extends: ['plugin:vue/recommended', 'plugin:boilerplate/base', "eslint:recommended"],
    plugins: [
      'babel',
    ],
  }

了解更多 vue AST 知识,可以查看

实践开发 TypeScirpt 模板 Eslint plugin

2019 年 1 月,TypeScirpt 官方决定全面采用 ESLint 作为代码检查的工具,并创建了一个新项目 typescript-eslint,提供了 TypeScript 文件的解析器 @typescript-eslint/parser 和相关的配置选项 @typescript-eslint/eslint-plugin 等。之前的两个 lint 解决方案已弃用:

我们在开发 TypeScript Eslint,也有很多生态工具,帮助我们快速上手:

TypeScript ESTree —— 将 TypeScript 源代码转换为 ESTree 兼容形式的解析器。

Utils for ESLint Plugins —— TypeScript + ESLint的实用工具。

Name Description
ASTUtils 操作 ESTree AST 的工具
ESLintUtils 使用 TypeScript 创建 ESLint 规则的工具
JSONSchema 引入 @types/json-schema 工具,json 形式定义配置
TSESLint TS 的 ESLint 的类型
TSESLintScope 依托 eslint-scope,创建的 TSESLintScope
TSESTree @typescript-eslint/typescript-estree 解析出的 TSESTree
AST_NODE_TYPES TSESTree 提供的 node type
AST_TOKEN_TYPES TSESTree 提供的 token type
ParserServices typescript 解析器使用的是 @typescript-eslint/typescript-estreeimport { ESLintUtils, getParserServices } from '@typescript-eslint/experimental-utils';​import * as tsutils from 'tsutils';​export default ESLintUtils.RuleCreator( name =>   https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin/docs/rules/${name}.md,)({ name: 'await-thenable', meta: {   docs: {     description: 'Disallows awaiting a value that is not a Thenable',     category: 'Best Practices',     recommended: 'error',     requiresTypeChecking: true,   },   messages: {     await: 'Unexpected await of a non-Promise (non-"Thenable") value.',   },   schema: [],   type: 'problem', }, defaultOptions: [],​ create(context) {   const parserServices = getParserServices(context);   const checker = parserServices.program.getTypeChecker();​   return {     AwaitExpression(node): void {       const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);       const type = checker.getTypeAtLocation(originalNode.expression);​       if (         !util.isTypeAnyType(type) &&         !util.isTypeUnknownType(type) &&         !tsutils.isThenableType(checker, originalNode.expression, type)       ) {         context.report({           messageId: 'await',           node,         });       }     },   }; },});module.exports = {   configs: {     base: {       "parser": "@typescript-eslint/parser",       "parserOptions": {           "sourceType": "module",           "allowImportExportEverywhere": false       },       plugins: ['@typescript-eslint'],       rules: {         '@typescript-eslint/adjacent-overload-signatures': 'error',         '@typescript-eslint/array-type': 'error',         '@typescript-eslint/await-thenable': 'error',       }     }   },   env: {     browser: true,     es6: true   },   rules: {     '@typescript-eslint/adjacent-overload-signatures': 'error',     '@typescript-eslint/array-type': 'error',     '@typescript-eslint/await-thenable': 'error',   } }Other Resource扩展eslint-plugin-boilerplate

可以通过使用 astexplorer.net,选择 @typescript-eslint/parser 解析器,去了解 ESLint 如何使用 评估校验代码 TypeScript 代码,从而方便我们快速开发 rules。

微软 TypeScript 团队开始正式支持通过 Babel 进行的 TypeScript 解析, @typescript-eslint/parser 解析器后面会逐步被 AST 与 Babel 解析器生成的AST进行替代。目前市面上挺多 TypeScript AST 解析的,如 ts-ast-viewer

  • TypeScript 插件规则,示例
import { ESLintUtils, getParserServices } from '@typescript-eslint/experimental-utils';

import * as tsutils from 'tsutils';

export default ESLintUtils.RuleCreator(
  name =>
    `https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin/docs/rules/${name}.md`,
)({
  name: 'await-thenable',
  meta: {
    docs: {
      description: 'Disallows awaiting a value that is not a Thenable',
      category: 'Best Practices',
      recommended: 'error',
      requiresTypeChecking: true,
    },
    messages: {
      await: 'Unexpected `await` of a non-Promise (non-"Thenable") value.',
    },
    schema: [],
    type: 'problem',
  },
  defaultOptions: [],

  create(context) {
    const parserServices = getParserServices(context);
    const checker = parserServices.program.getTypeChecker();

    return {
      AwaitExpression(node): void {
        const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
        const type = checker.getTypeAtLocation(originalNode.expression);

        if (
          !util.isTypeAnyType(type) &&
          !util.isTypeUnknownType(type) &&
          !tsutils.isThenableType(checker, originalNode.expression, type)
        ) {
          context.report({
            messageId: 'await',
            node,
          });
        }
      },
    };
  },
});
  • TypeScript 插件入口文件示例:
module.exports = {
    configs: {
      base: {
        "parser": "@typescript-eslint/parser",
        "parserOptions": {
            "sourceType": "module",
            "allowImportExportEverywhere": false
        },
        plugins: ['@typescript-eslint'],
        rules: {
          '@typescript-eslint/adjacent-overload-signatures': 'error',
          '@typescript-eslint/array-type': 'error',
          '@typescript-eslint/await-thenable': 'error',
        }
      }
    },
    env: {
      browser: true,
      es6: true
    },
    rules: {
      '@typescript-eslint/adjacent-overload-signatures': 'error',
      '@typescript-eslint/array-type': 'error',
      '@typescript-eslint/await-thenable': 'error',
    }
  }

eslint-plugin-boilerplate

eslint-plugin-boilerplate —— 快速开发 eslint plugins 模版样例。

扩展

stylelint 插件开发

Other Resource

ruletester

Eslint

The ESLint Vue Plugin Developer Guide

Working with Rules

开发一个 plugin 插件

Shareable Configs

vue-eslint-parser AST docs

【AST篇】教你如何动手写 Eslint 插件