Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

自定义 Eslint 开发 #70

Open
pfan123 opened this issue Jun 16, 2020 · 0 comments
Open

自定义 Eslint 开发 #70

pfan123 opened this issue Jun 16, 2020 · 0 comments

Comments

@pfan123
Copy link
Owner

pfan123 commented Jun 16, 2020

对于前端开发者来说,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 插件

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant