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: {
plugins: ['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
这是因为解析器需要用绝对路径 配置 ESLint RuleTester 以使用 Typescript Parser
/**
* @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
执行测试用例,控制台输出:
github
传送门:https://github.com/Revelation...
在VSCode中调试测试用例
依照下面流程在工程创建launch.json
,会在根目录下生成.vscode
目录
launch.json
默认内容如下:
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}\\lib\\index.js" // debugger默认启动的脚本
}
]
}
这里我们想调试no-interface-comments
规则的测试用例,需做如下更改:
"program": "${workspaceFolder}\\tests\\lib\\rules\\no-interface-comments.js"
然后,打断点debugger
就可以调试了
项目中使用
(1)安装eslint-plugin-pony-comments
yarn add eslint-plugin-pony-comments -D
(2)配置.eslintrc.json
{
"extends": "plugin:@casstime/inquiry-eslint/recommended",
"parser": "@typescript-eslint/parser",
}
(3)重启项目,重启eslint
服务,检视效果如下
如果在yarn start
启动项目时报了很多未更改文件的校验错误,这是因为create-react-app
脚手架中默认配置了eslint-loader
,在start
或者build
时会调用eslint
去将所有文件检视一遍,参考:https://v4.webpack.docschina....
针对以上问题,这里提供两种解决办法:
- 删除
node_modules/.cache
文件夹,然后重启项目。这是因为eslint-loader
默认启动cache
,将linting
结果缓存写入./node_modules/.cache
目录,这对于在进行完整构建时减少linting
时间特别有用,但可能影响到start
或者build
时eslint
的检视 - 在根目录下
config-overrides.js
的override
函数中添加config.module.rules.splice(1, 1);
,因为编写代码和提交代码时就会检视,不用在项目启动或者构建时再检视一遍,直接干掉eslint-loader
发布
(1)npm
账户登录
npm login --registry=仓库镜像
# 输入用户名/密码/邮箱
(2)执行构建
yarn build
(3)发布
yarn publish --registry=仓库镜像
如果报如下错误,可能是使用的邮箱不对
课外知识: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》
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
的前端主流工具。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。