从AST原理到ESlint实践

AST(抽象语法树) 为什么要谈AST?
如果你查看目前任何主流的项目中的devDependencies,会发现前些年的不计其数的插件诞生。我们归纳一下有:ES6转译、代码压缩、css预处理器、eslintprettier等。这些模块很多都不会用到生产环境,但是它们在开发环境中起到很重要的作用,这些工具的诞生都是建立在了AST这个巨人的肩膀上。
从AST原理到ESlint实践
文章图片

什么是AST?

It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.
抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这所以说是抽象的,是因为抽象语法树并不会表示出真实语法出现的每一个细节,比如说,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。抽象语法树并不依赖于源语言的语法,也就是说语法分析阶段所采用的上下文无文文法,因为在写文法时,经常会对文法进行等价的转换(消除左递归,回溯,二义性等),这样会给文法分析引入一些多余的成分,对后续阶段造成不利影响,甚至会使合个阶段变得混乱。因些,很多编译器经常要独立地构造语法分析树,为前端,后端建立一个清晰的接口。 从AST原理到ESlint实践
文章图片

从纯文本转换成树形结构的数据,也就是AST,每个条目和树中的节点一一对应。
AST的流程
此部分将让你了解到从源代码到词法分析生成tokens再到语法分析生成AST的整个流程。
从源代码中怎么得到AST呢?当下的编译器帮着做了这件事,那编译器是怎么做的呢?
从AST原理到ESlint实践
文章图片

一款编译器的编译流程(将高级语言转译成二进制位)是很复杂的,但我们只需要关注词法分析和语法分析,这两步是从代码生成AST的关键所在。
从AST原理到ESlint实践
文章图片

第一步,词法分析器,也称为扫描器,它会先对整个代码进行扫描,当它遇到空格、操作符或特殊符号时,它决定一个单词完成,将识别出的一个个单词、操作符、符号等以对象的形式({type, value, range, loc })记录在tokens数组中,注释会另外存放在一个comments数组中。
从AST原理到ESlint实践
文章图片

比如var a = 1; @typescript-eslint/parser解析器生成的tokens如下:
tokens: [ { "type": "Keyword", "value": "var", "range": [112, 115], "loc": { "start": { "line": 11, "column": 0 }, "end": { "line": 11, "column": 3 } } }, { "type": "Identifier", "value": "a", "range": [116, 117], "loc": { "start": { "line": 11, "column": 4 }, "end": { "line": 11, "column": 5 } } }, { "type": "Punctuator", "value": "=", "range": [118, 119], "loc": { "start": { "line": 11, "column": 6 }, "end": { "line": 11, "column": 7 } } }, { "type": "Numeric", "value": "1", "range": [120, 121], "loc": { "start": { "line": 11, "column": 8 }, "end": { "line": 11, "column": 9 } } }, { "type": "Punctuator", "value": "; ", "range": [121, 122], "loc": { "start": { "line": 11, "column": 9 }, "end": { "line": 11, "column": 10 } } } ]

第二步,语法分析器,也称为解析器,将词法分析得到的tokens数组转换为树形结构表示,验证语言语法并抛出语法错误(如果发生这种情况)
从AST原理到ESlint实践
文章图片

var a = 1; tokens数组转换为树形结构如下所示:
{ type: 'Program', body: [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a", "range": [ 116, 117 ], "loc": { "start": { "line": 11, "column": 4 }, "end": { "line": 11, "column": 5 } } }, "init": { "type": "Literal", "value": 1, "raw": "1", "range": [ 120, 121 ], "loc": { "start": { "line": 11, "column": 8 }, "end": { "line": 11, "column": 9 } } }, "range": [ 116, 121 ], "loc": { "start": { "line": 11, "column": 4 }, "end": { "line": 11, "column": 9 } } } ], "kind": "var", "range": [ 112, 122 ], "loc": { "start": { "line": 11, "column": 0 }, "end": { "line": 11, "column": 10 } } } ] }

在生成树时,解析器会剔除掉一些不必要的标记(例如冗余括号),因此创建的“抽象语法树”不是 100% 与源代码匹配,但足以让我们知道如何处理它。另一方面,完全覆盖所有代码结构的解析器生成的树称为“具体语法树”
从AST原理到ESlint实践
文章图片

编译器拓展
想了解更多关于编译器的知识? the-super-tiny-compiler,这是一个用JavaScript 编写的编译器。大概200行代码,其背后的想法是将Lisp编译成C语言,几乎每行都有注释。
从AST原理到ESlint实践
文章图片

LangSandbox,一个更好的项目,它说明了如何创造一门编程语言。当然,设计编程语言这样的书市面上也一坨坨。所以,这项目更加深入,与the-super-tiny-compiler的项目将Lisp转为C语言不同,这个项目你可以写一个你自己的语言,并且将它编译成C语言或者机器语言,最后运行它。
从AST原理到ESlint实践
文章图片

能直接用三方库来生成AST吗? 当然可以!有一堆三方库可以用。你可以访问astexplorer,然后挑你喜欢的库。astexplorer是一个很棒的网站,你可以在线玩转AST,而且除了JavaScript 之外,它还包含许多其他语言AST
从AST原理到ESlint实践
文章图片

我想特别强调其中的一个,在我看来它是非常好的一个,babylon
从AST原理到ESlint实践
文章图片

它在 Babel 中使用,也许这也是它受欢迎的原因。因为它是由 Babel项目支持的,所以它会始终与最新的JS特性保持同步,可以大胆地使用。另外,它的API也非常的简单,容易使用。
OK,现在您知道如何将代码生成 AST,让我们继续讨论现实中的用例。
我想谈论的第一个用例是代码转译,当然是 Babel
Babel is not a ‘tool for having ES6 support’. Well, it is, but it is far not only what it is about.
BabelES6/7/8 特性的支持有很多关联,这就是我们经常使用它的原因。但它仅仅是一组插件,我们还可以将它用于代码压缩、React 相关的语法转换(例如 JSX)、Flow 插件等。
从AST原理到ESlint实践
文章图片

Babel 是一个 JavaScript编译器,它的编译有三个阶段:解析(parsing)、转译(transforming)、生成(generation)。你给 Babel一些 JavaScript 代码,它修改代码并生成新的代码,它是如何修改代码?没错!它构建 AST,遍历它,根据babel-plugin修改它,然后从修改后的AST 生成新代码。
让我们在一个简单的代码示例中看到这一点。
从AST原理到ESlint实践
文章图片

正如我之前提到的,Babel 使用 Babylon,所以,我们首先解析代码生成AST,然后遍历 AST 并反转所有变量名称,最后生成代码。正如我们看到的,第一步(解析)和第三步(代码生成)阶段看起来很常见,每次都会做的。所以,Babel接管了这两步,我们真正感兴趣的是 AST 转换(Babel-plugin修改)。
当开发 Babel-plugin 时,你只需要描述节点“visitors”,它会改变你的AST。将它加入你的babel插件列表中,设置你webpackbabel-loader配置或者.babelrc中的plugins即可
从AST原理到ESlint实践
文章图片

如果你想了解更多关于如何创建 babel-plugin,你可以查看 Babel-handbook。
从AST原理到ESlint实践
文章图片

AST 在 ESLint 中的运用 在正式写 ESLint 插件前,你需要了解下 ESLint 的工作原理。其中 ESLint 使用方法大家应该都比较熟悉,这里不做讲解,不了解的可以点击官方文档 如何在项目中配置 ESLint。
在项目开发中,不同开发者书写的源码是各不相同的,那么 ESLint 如何去分析每个人写的源码呢?
没错,就是 AST (Abstract Syntax Tree(抽象语法树)),再祭上那张看了几百遍的图。
从AST原理到ESlint实践
文章图片

【从AST原理到ESlint实践】ESLint 中,默认使用 esprima 来解析 Javascript ,生成抽象语法树,然后去 拦截 检测是否符合我们规定的书写方式,最后让其展示报错、警告或正常通过。 ESLint 的核心就是规则(rules),而定义规则的核心就是利用 AST 来做校验。每条规则相互独立,可以设置禁用off、警告warn??和报错error?,当然还有正常通过不用给任何提示。
手把手教你写Eslint插件 目标&涉及知识点
本文 ESLint 插件旨在校验代码注释是否写了注释:
  • 每个声明式函数、函数表达式都需要注释;
  • 每个interface头部和字段都需要注释;
  • 每个enum头部和字段都需要注释;
  • 每个type头部都需要注释;
  • ......
知识点
  • AST 抽象语法树
  • ESLint
  • Mocha单元测试
  • Npm 发布
脚手架搭建项目
这里我们利用 yeoman 和 generator-eslint 来构建插件的脚手架代码,安装:
npm install -g yo generator-eslint

本地新建文件夹eslint-plugin-pony-comments
mkdir eslint-plugin-pony-comments cd eslint-plugin-pony-comments

命令行初始化ESLint插件的项目结构:
yo eslint:plugin

下面进入命令行交互流程,流程结束后生成ESLint插件项目框架和文件
$ yo eslint:plugin ? What is your name? xxx // 作者 ? What is the plugin ID? eslint-plugin-pony-comments // 插件名称 ? Type a short description of this plugin: 检查代码注释 // 插件描述 ? Does this plugin contain custom ESLint rules? (Y/n) Y ? Does this plugin contain custom ESLint rules? Yes // 这个插件是否包含自定义规则 ? Does this plugin contain one or more processors? (y/N) N ? Does this plugin contain one or more processors? No // 该插件是否需要处理器 create package.json create lib\index.js create README.md

此时文件的目录结构为:
. ├── README.md ├── lib │├── processors // 处理器,选择不需要时没有该目录 │├── rules // 自定义规则目录 │└── index.js // 导出规则、处理器以及配置 ├── package.json └── tests ├── processors // 处理器,选择不需要时没有该目录 └── lib └── rules // 编写规则的单元测试用例

安装依赖:
npm install // 或者yarn

至此,环境搭建完毕。
创建规则
以实现”每个interface头部和字段都需要注释“为例创建规则,终端执行:
yo eslint:rule // 生成默认 eslint rule 模版文件

下面进入命令行交互流程:
$ yo eslint:rule ? What is your name? xxx // 作者 ? Where will this rule be published? ESLint Plugin // 选择生成插件模板 ? What is the rule ID? no-interface-comments // 规则名称 ? Type a short description of this rule: 校验interface注释 // 规则描述 ? Type a short example of the code that will fail: create docs\rules\no-interface-comments.md create lib\rules\no-interface-comments.js create tests\lib\rules\no-interface-comments.js

此时项目结构为:
. ├── README.md ├── docs // 说明文档 │└── rules │└── no-interface-comments.md ├── lib // eslint 规则开发 │├── index.js │└── rules // 此目录下可以构建多个规则,本文只拿一个规则来讲解 │└── no-interface-comments.js ├── package.json └── tests // 单元测试 └── lib └── rules └── no-interface-comments.js

ESLint 中的每个规则都有三个以其标识符命名的文件(例如,no-interface-comments)。
  • lib/rules目录中:一个源文件(例如,no-interface-comments.js
  • tests/lib/rules目录中:一个测试文件(例如,no-interface-comments.js
  • docs/rules目录中:一个 Markdown 文档文件(例如,no-interface-comments
在正式进入开发规则之前先来看看生成的规则模板 no-interface-comments.js
/** * @fileoverview no-interface-comments * @author xxx */ "use strict"; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------module.exports = { meta: { docs: { description: "no console.time()", category: "Fill me in", recommended: false }, fixable: null,// 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 {// give me methods}; } };

这个文件给出了书写规则的模版,一个规则对应一个可导出的 node 模块,它由 metacreate 两部分组成。
  • meta:代表了这条规则的元数据,如其类别,文档,可接收的参数的 schema 等等。
  • create:如果说 meta 表达了我们想做什么,那么 create 则用表达了这条 rule 具体会怎么分析代码;
create 返回一个对象,其中最常见的键名是AST抽象语法树中的选择器,在该选择器中,我们可以获取对应选中的内容,随后我们可以针对选中的内容作一定的判断,看是否满足我们的规则。如果不满足,可用 context.report 抛出问题,ESLint 会利用我们的配置对抛出的内容做不同的展示。详情参考:context.report
在编写no-interface-comments规则之前,我们在AST Explorer看看interface代码解析成AST的结构是怎么样的?
从AST原理到ESlint实践
文章图片

根据上面AST结构,我们创建两个选择器校验代码注释,TSInterfaceDeclaration选择器校验interface头部是否有注释,TSPropertySignature选择器校验字段是否有注释。遍历AST可能需要用到以下API,详情参考官网:
  • fixer.insertTextAfter(nodeOrToken, text) - 在给定的节点或标记之后插入文本
  • fixer.insertTextBefore(nodeOrToken, text) - 在给定的节点或标记之前插入文本
  • sourceCode.getAllComments() - 返回源代码中所有注释的数组
  • context.getSourceCode() - 获取源代码
/** * @fileoverview interface定义类型注释校验 * @author xxx */ 'use strict'; const { docsUrl, getLastEle, getAllComments, judgeNodeType, getComments, genHeadComments, report, isTailLineComments, getNodeStartColumn, genLineComments, } = require('../utils'); module.exports = { meta: { /** * 规则的类型 * "problem" 意味着规则正在识别将导致错误或可能导致混淆行为的代码。开发人员应将此视为优先解决的问题。 * "suggestion" 意味着规则正在确定可以以更好的方式完成的事情,但如果不更改代码,则不会发生错误。 * "layout" 意味着规则主要关心空格、分号、逗号和括号,程序的所有部分决定了代码的外观而不是它的执行方式。这些规则适用于 AST 中未指定的部分代码。 */ type: 'layout', docs: { description: 'interface定义类型注释校验', // 规则描述 category: 'Fill me in', recommended: true, // 是配置文件中的"extends": "eslint:recommended"属性是否启用规则 url: 'https://github.com/Revelation2019/eslint-plugin-pony-comments/tree/main/docs/rules/no-interface-comments.md', // 该规则对应在github上的文档介绍 }, fixable: 'whitespace',// or "code" or "whitespace" schema: [ // 指定选项,比如'pony-comments/no-interface-comments: [2, 'always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block'}}]' { 'enum': ['always', 'never'], }, { 'type': 'object', 'properties': { /** * 是否需要头部注释 * 'No':表示不需要头部注释 * 'Line': 表示头部需要单行注释 * 'Block':表示头部需要多行注释 */ 'leadingCommentType': { 'type': 'string', }, /** 字段注释采用单行还是多行注释 */ 'propertyComments': { 'type': 'object', 'properties': { 'pos': { 'type': 'string', // lead || tail 表示注释位置是行头还是行尾 }, 'commentsType': { 'type': 'string', // No || Line || Block 表示注释是单行还是多行,或者不需要注释 }, }, }, }, 'additionalProperties': false, }, ], },create: function(context) { // 获取选项 const options = context.options; const leadingCommentsType = options.length > 0 ? getLastEle(options).leadingCommentType : null; const propertyComments = options.length > 0 ? getLastEle(options).propertyComments : {}; const { pos, commentsType } = propertyComments; /** 获取所有的注释节点 */ const comments = getAllComments(context); // 有效的选项值 const commentsTypeArr = ['No', 'Line', 'Block']; return { /** 校验interface定义头部注释 */ 'TSInterfaceDeclaration': (node) => { /** 不需要头部注释 */ if (leadingCommentsType === 'No' || !commentsTypeArr.includes(leadingCommentsType)) return; const { id } = node; const { name } = id; // 判断interface的父节点是否是export if (judgeNodeType(node, 'ExportNamedDeclaration')) { /** export interface XXX {} */ const { leading } = getComments(context, node.parent); if (!leading.length) { // 没有头部注释,抛出断言 report(context, node.parent, '导出的类型头部没有注释', genHeadComments(node.parent, name, leadingCommentsType)); } } else { /** enum interface {} */ const { leading } = getComments(context, node); // 获取节点头部和尾部注释 if (!leading.length) { // 没有头部注释,抛出断言 report(context, node, '类型头部没有注释', genHeadComments(node, name, leadingCommentsType)); } } }, /** 校验interface定义字段注释 */ 'TSPropertySignature': (node) => { if (commentsType === 'No' || !commentsTypeArr.includes(commentsType)) return; /** 避免 export const Main = (props: { name: string }) => {} */ if (judgeNodeType(node, 'TSInterfaceBody')) { const { key } = node; const { name } = key; const { leading } = getComments(context, node); // 获取节点头部和尾部注释 const errorMsg = '类型定义的字段没有注释'; if (isTailLineComments(comments, node) || (leading.length &&getNodeStartColumn(getLastEle(leading)) === getNodeStartColumn(node))) { /** * 节点尾部已有注释 或者 头部有注释并且注释开头与节点开头列数相同 * 这里判断节点开始位置column与注释开头位置column是因为getComments获取到的头部注释可能是不是当前节点的,比如 interface xxx { id: string; // id name: string; // name } leading拿到的是// id,但这个注释不是name字段的 */ return; } // 根据选项报出断言,并自动修复 if (commentsType === 'Block' || (commentsType === 'Line' && pos === 'lead')) { // 自动添加行头多行注释 report(context, node, errorMsg, genHeadComments(node, name, commentsType)); } else { // 自动添加行尾单行注释 report(context, node, errorMsg, genLineComments(node, name)); } } }, }; }, };

自动修复函数:
/** * @description 在函数头部加上注释 * @param {Object} node 当前节点 * @param {String} text 注释内容 * @returns */ const genHeadComments = (node, text, commentsType) => { if (!text) return null; const eol = require('os').EOL; // 获取换行符,window是CRLF,linux是LF let content = ''; if (commentsType && commentsType.toLowerCase === 'line') { content = `// ${text}${eol}`; } else if (commentsType && commentsType.toLowerCase === 'block') { content = `/** ${text} */${eol}`; } else { content = `/** ${text} */${eol}`; } return (fixer) => { return fixer.insertTextBefore( node, content, ); }; };

/** * @description 生成行尾单行注释 * @param {Object} node 当前节点 * @param {String} value 注释内容 * @returns */ const genLineComments = (node, value) => { return (fixer) => { return fixer.insertTextAfter( node, `// ${value}`, ); }; };

至此,no-interface-comments规则编写就基本完成了
插件中的配置
您可以通过在configs键下指定它们来将配置捆绑在插件中。当您不仅要提供代码样式,而且要提供一些支持它的自定义规则时,这会很有用。每个插件支持多种配置。请注意,无法为给定插件指定默认配置,用户必须在其配置文件中指定何时使用。参考官网
// lib/index.js module.exports = { configs: { recommended: { plugin: 'pony-comments', parserOptions: { sourceType: 'module', ecmaVersion: 2018, }, rules: { 'pony-comments/no-interface-comments': [2, 'always', { leadingCommentType: 'Block', propertyComments: { pos: 'tail', commentsType: 'Line' } }], } }, } };

插件规则将可以通过extends配置继承:
{ "extends": ["plugin:pony-comments/recommended"] }

注意:请注意,默认情况下配置不会启用任何插件规则,而是应视为独立配置。这意味着您必须在plugins数组中指定您的插件名称以及您要启用的任何规则,这些规则是插件的一部分。任何插件规则都必须以短或长插件名称作为前缀
创建处理器
处理器可以告诉 ESLint 如何处理 JavaScript 以外的文件,比如从其他类型的文件中提取 JavaScript 代码,然后让 ESLintJavaScript 代码进行 lint,或者处理器可以出于某种目的在预处理中转换 JavaScript 代码。参考官网
// 在lib/index.js中导出自定义处理器,或者将其抽离 module.exports = { processors: { "markdown": { // 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 lintreturn [ // return an array of code blocks to lint { text: code1, filename: "0.js" }, { text: code2, filename: "1.js" }, ]; },// 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 [].concat(...messages); },supportsAutofix: true // (optional, defaults to false) } } };

要在配置文件中指定处理器,请使用processor带有插件名称和处理器名称的连接字符串的键(由斜杠)。例如,以下启用pony-comments插件提供的markdown处理器:
{ "plugins": ["pony-comments"], "processor": "pony-comments/markdown" }

要为特定类型的文件指定处理器,请使用overrides键和processor键的组合。例如,以下使用处理器pony-comments/markdown处理*.md文件。
{ "plugins": ["pony-comments"], "overrides": [ { "files": ["*.md"], "processor": "pony-comments/markdown" } ] }

处理器可能会生成命名代码块,例如0.js1.jsESLint 将这样的命名代码块作为原始文件的子文件处理。您可以overridesconfig 部分为命名代码块指定其他配置。例如,以下strict代码禁用.jsmarkdown 文件结尾的命名代码块的规则。
{ "plugins": ["pony-comments"], "overrides": [ { "files": ["*.md"], "processor": "pony-comments/markdown" }, { "files": ["**/*.md/*.js"], "rules": { "strict": "off" } } ] }

ESLint 检查命名代码块的文件路径,如果任何overrides条目与文件路径不匹配,则忽略那些。一定要加的overrides,如果你想皮棉比其他命名代码块的条目*.js
文件扩展名处理器 如果处理器名称以 开头.,则 ESLint 将处理器作为文件扩展名处理器来处理,并自动将处理器应用于文件类型。人们不需要在他们的配置文件中指定文件扩展名的处理器。例如:
module.exports = { processors: { // This processor will be applied to `*.md` files automatically. // Also, people can use this processor as "plugin-id/.md" explicitly. ".md": { preprocess(text, filename) { /* ... */ }, postprocess(messageLists, filename) { /* ... */ } } } }

编写单元测试
eslint.RuleTester是一个为 ESLint 规则编写测试的实用程序。RuleTester构造函数接受一个可选的对象参数,它可以用来指定测试用例的默认值(官网)。例如,如果可以指定用@typescript-eslint/parser解析你的测试用例:
const ruleTester = new RuleTester({ parser: require.resolve('@typescript-eslint/parser') });

当需要解析.tsx文件时,就需要指定特定的解析器,比如@typescript-eslint/parser,因为eslint服务默认使用的解析器是esprima,它不支持对typescriptreact
如果在执行测试用例时报如下错误:
AssertionError [ERR_ASSERTION]: Parsers provided as strings to RuleTester must be absolute paths

这是因为解析器需要用绝对路径
/** * @fileoverview interface定义类型注释校验 * @author xxx */ 'use strict'; const rule = require('../../../lib/rules/no-interface-comments'); const RuleTester = require('eslint').RuleTester; const ruleTester = new RuleTester({ parser: require.resolve('@typescript-eslint/parser'), parserOptions: { ecmaVersion: 2018, sourceType: 'module', ecmaFeatures: { jsx: true, }, comment: true, useJSXTextNode: true, }, }); ruleTester.run('no-interface-comments', rule, { // 有效测试用例 valid: [ { code: ` export const Main = (props: { name: string }) => {} `, options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block' } }], }, { code: ` /** 类型 */ export interface IType { id: string; // id name: string; // 姓名 age: number; // 年龄 } `, options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'tail', commentsType: 'Line' } }], }, { code: ` /** 类型 */ interface IType { /** id */ id: string; /** 姓名 */ name: string; /** 年龄 */ age: number; } `, options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block' } }], }, ], // 无效测试用例 invalid: [ { code: ` export interface IType { /** id */ id: string; /** 姓名 */ name: string; /** 年龄 */ age: number; } `, errors: [{ message: 'interface头部必须加上注释', type: 'TSInterfaceDeclaration', }], options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block' } }], output: ` /** 类型 */ export interface IType { /** id */ id: string; /** 姓名 */ name: string; /** 年龄 */ age: number; } `, }, { code: ` /** 类型 */ interface IType { id: string; name: string; age: number; } `, errors: [{ message: 'interface字段必须加上注释', type: 'TSPropertySignature', }], options: ['always', { leadingCommentType: 'Block', propertyComments: { pos: 'lead', commentsType: 'Block' } }], output: ` /** 类型 */ interface IType { /** id */ id: string; /** 姓名 */ name: string; /** 年龄 */ age: number; } `, }, ], });

yarn test执行测试用例,控制台输出:
从AST原理到ESlint实践
文章图片

课外知识:Lint 简史 Lint 是为了解决代码不严谨而导致各种问题的一种工具。比如 ===== 的混合使用会导致一些奇怪的问题。
JSLint 和 JSHint
2002年,Douglas Crockford 开发了可能是第一款针对 JavaScript 的语法检测工具 —— JSLint,并于 2010 年开源。
JSLint 面市后,确实帮助许多 JavaScript 开发者节省了不少排查代码错误的时间。但是 JSLint 的问题也很明显—— 几乎不可配置,所有的代码风格和规则都是内置好的;再加上 Douglas Crockford 推崇道系「爱用不用」的优良传统,不会向开发者妥协开放配置或者修改他觉得是对的规则。于是 Anton Kovalyov 吐槽:「JSLint 是让你的代码风格更像 Douglas Crockford 的而已」,并且在 2011 年 Fork 原项目开发了 JSHint。《Why I forked JSLint to JSHint》
JSHint 的特点就是可配置,同时文档也相对完善,而且对开发者友好。很快大家就从 JSLint 转向了 JSHint
ESLint 的诞生
后来几年大家都将 JSHint 作为代码检测工具的首选,但转折点在2013年,Zakas 发现 JSHint 无法满足自己制定规则需求,并且和 Anton 讨论后发现这根本不可能在JShint上实现,同时 Zakas 还设想发明一个基于 ASTlint。于是 2013年6月份,Zakas 发布了全新 lint 工具——ESLint。《Introducing ESLint》
ESLint早期源码:
var ast = esprima.parse(text, { loc: true, range: true }), walk = astw(ast); walk(function(node) { api.emit(node.type, node); }); return messages;

ESLint 的逆袭
ESLint 的出现并没有撼动 JSHint 的霸主地位。由于前者是利用 AST 处理规则,用 Esprima 解析代码,执行速度要比只需要一步搞定的 JSHint 慢很多;其次当时已经有许多编辑器对 JSHint 支持完善,生态足够强大。真正让 ESLint 逆袭的是 ECMAScript 6 的出现。
2015 年 6 月,ES2015 规范正式发布。但是发布后,市面上浏览器对最新标准的支持情况极其有限。如果想要提前体验最新标准的语法,就得靠 Babel 之类的工具将代码编译成 ES5 甚至更低的版本,同时一些实验性的特性也能靠 Babel 转换。 但这时候的 JSHint 短期内无法提供支持,而 ESLint 却只需要有合适的解析器就能继续去 lint 检查。Babel 团队就为 ESLint 开发了一款替代默认解析器的工具,也就是现在我们所见到的 babel-eslint,它让 ESLint 成为率先支持 ES6 语法的 lint 工具。
也是在 2015 年,React 的应用越来越广泛,诞生不久的 JSX 也愈加流行。ESLint 本身也不支持 JSX 语法。但是因为可扩展性,eslint-plugin-react 的出现让 ESLint 也能支持当时 React 特有的规则。
2016 年,JSCS 开发团队认为 ESLint JSCS 实现原理太过相似,而且需要解决的问题也都一致,最终选择合并到 ESLint,并停止 JSCS 的维护。
当前市场上主流的 lint 工具以及趋势图:
从AST原理到ESlint实践
文章图片

从此 ESLint 一统江湖,成为替代 JSHint 的前端主流工具。
参考: 平庸前端码农之蜕变 — AST
【AST篇】手把手教你写Eslint插件
配置 ESLint RuleTester 以使用 Typescript Parser

    推荐阅读