从AST原理到ESlint实践
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
(抽象语法树)),再祭上那张看了几百遍的图。文章图片
【从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
模块,它由 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 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.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
推荐阅读
- Docker应用:容器间通信与Mariadb数据库主从复制
- 一个人的碎碎念
- 我从来不做坏事
- 做一件事情的基本原理是什么()
- 从蓦然回首到花开在眼前,都是为了更好的明天。
- 西湖游
- 改变自己,先从自我反思开始
- leetcode|leetcode 92. 反转链表 II
- 从我的第一张健身卡谈传统健身房
- CGI,FastCGI,PHP-CGI与PHP-FPM