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-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
模块,它由 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.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
代码,而后让 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