AST(形象语法树)
为什么要谈AST?
如果你查看目前任何支流的我的项目中的devDependencies
,会发现前些年的成千上万的插件诞生。咱们演绎一下有:ES6
转译、代码压缩、css
预处理器、eslint
、prettier
等。这些模块很多都不会用到生产环境,然而它们在开发环境中起到很重要的作用,这些工具的诞生都是建设在了AST
这个伟人的肩膀上。
什么是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
,每个条目和树中的节点一一对应。
AST的流程
此局部将让你理解到从源代码到词法剖析生成tokens
再到语法分析生成AST
的整个流程。
从源代码中怎么失去AST
呢?当下的编译器帮着做了这件事,那编译器是怎么做的呢?
一款编译器的编译流程(将高级语言转译成二进制位)是很简单的,但咱们只须要关注词法剖析和语法分析,这两步是从代码生成AST
的关键所在。
第一步,词法分析器,也称为扫描器,它会先对整个代码进行扫描,当它遇到空格、操作符或特殊符号时,它决定一个单词实现,将辨认出的一个个单词、操作符、符号等以对象的模式({type, value, range, loc }
)记录在tokens
数组中,正文会另外寄存在一个comments
数组中。
比方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
数组转换为树形构造示意,验证语言语法并抛出语法错误(如果产生这种状况)
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% 与源代码匹配,但足以让咱们晓得如何解决它。另一方面,齐全笼罩所有代码构造的解析器生成的树称为“具体语法树”
编译器拓展
想理解更多对于编译器的常识? the-super-tiny-compiler,这是一个用JavaScript
编写的编译器。大略200行代码,其背地的想法是将Lisp
编译成C
语言,简直每行都有正文。
LangSandbox,一个更好的我的项目,它阐明了如何发明一门编程语言。当然,设计编程语言这样的书市面上也一坨坨。所以,这我的项目更加深刻,与the-super-tiny-compiler的我的项目将Lisp
转为C
语言不同,这个我的项目你能够写一个你本人的语言,并且将它编译成C
语言或者机器语言,最初运行它。
能间接用三方库来生成AST
吗? 当然能够!有一堆三方库能够用。你能够拜访astexplorer,而后挑你喜爱的库。astexplorer
是一个很棒的网站,你能够在线玩转AST
,而且除了JavaScript
之外,它还蕴含许多其余语言AST
库
我想特别强调其中的一个,在我看来它是十分好的一个,babylon
它在 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.
Babel
与ES6/7/8
个性的反对有很多关联,这就是咱们常常应用它的起因。但它仅仅是一组插件,咱们还能够将它用于代码压缩、React
相干的语法转换(例如 JSX
)、Flow
插件等。
Babel
是一个 JavaScript
编译器,它的编译有三个阶段:解析(parsing
)、转译(transforming
)、生成(generation
)。你给 Babel
一些 JavaScript
代码,它批改代码并生成新的代码,它是如何批改代码?没错!它构建 AST
,遍历它,依据babel-plugin
批改它,而后从批改后的AST
生成新代码。
让咱们在一个简略的代码示例中看到这一点。
正如我之前提到的,Babel
应用 Babylon
,所以,咱们首先解析代码生成AST
,而后遍历 AST
并反转所有变量名称,最初生成代码。正如咱们看到的,第一步(解析)和第三步(代码生成)阶段看起来很常见,每次都会做的。所以,Babel
接管了这两步,咱们真正感兴趣的是 AST
转换(Babel-plugin
批改)。
当开发 Babel-plugin
时,你只须要形容节点“visitors”
,它会扭转你的AST
。将它退出你的babel
插件列表中,设置你webpack
的babel-loader
配置或者.babelrc
中的plugins
即可
如果你想理解更多对于如何创立 babel-plugin
,你能够查看 Babel-handbook。
AST 在 ESLint 中的使用
在正式写 ESLint
插件前,你须要理解下 ESLint
的工作原理。其中 ESLint
应用办法大家应该都比拟相熟,这里不做解说,不理解的能够点击官网文档 如何在我的项目中配置 ESLint。
在我的项目开发中,不同开发者书写的源码是各不相同的,那么 ESLint
如何去剖析每个人写的源码呢?
没错,就是 AST
(Abstract Syntax Tree
(形象语法树)),再祭上那张看了几百遍的图。
在 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-commentscd 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
模块,它由 meta
和 create
两局部组成。
meta
:代表了这条规定的元数据,如其类别,文档,可接管的参数的schema
等等。create
:如果说meta
表白了咱们想做什么,那么create
则用表白了这条rule
具体会怎么剖析代码;
create
返回一个对象,其中最常见的键名是AST
形象语法树中的选择器,在该选择器中,咱们能够获取对应选中的内容,随后咱们能够针对选中的内容作肯定的判断,看是否满足咱们的规定。如果不满足,可用 context.report
抛出问题,ESLint
会利用咱们的配置对抛出的内容做不同的展现。详情参考:context.report
在编写no-interface-comments
规定之前,咱们在AST Explorer看看interface
代码解析成AST
的构造是怎么样的?
依据下面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.jsmodule.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
代码,而后让 ESLint
对 JavaScript
代码进行 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 lint return [ // 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.js
和1.js
。ESLint
将这样的命名代码块作为原始文件的子文件解决。您能够overrides
在 config
局部为命名代码块指定其余配置。例如,以下strict
代码禁用.js
以 markdown
文件结尾的命名代码块的规定。
{ "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
,它不反对对typescript
和react
如果在执行测试用例时报如下谬误:
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
执行测试用例,控制台输入:
课外常识: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
还构想创造一个基于 AST
的 lint
。于是 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
工具以及趋势图:
从此 ESLint
一统江湖,成为代替 JSHint
的前端支流工具。
参考:
平庸前端码农之变质 — AST
【AST篇】手把手教你写Eslint插件
配置 ESLint RuleTester 以应用 Typescript Parser