[关闭]
@lxjwlt 2016-10-09T09:12:20.000000Z 字数 4962 阅读 4343

ESlint自定义规则

博文


本文介绍ESlint自定义规则。

更好的阅读体验>>

ESlint默认规则

为了更好的学习ESlint自定义规则,我们应该多多参考ESlint的内置默认规则的写法。在ESlint源代码目录下我们能找到这些默认规则:

规则写法

规则模块暴露一个配置对象,其中定义规则的相关信息和具体实现,相关信息可以省略,但create方法其中实现规则具体的逻辑,不可省略:

  1. module.exports = {
  2. // context提供了很多实用方法,比如获取注释、获取源码...
  3. create: function (context) {
  4. return {
  5. CallExpression: function (node) {}
  6. };
  7. }
  8. };

在分析代码前,ESlint会通过Estree将代码解析一棵抽象语法树(AST),将不同类型的代码语句分成不同类型的节点,一份代码文件便形成了一个树状的结构,之后ESlint会依次遍历语法树上的节点。

create方法中要返回一个object,键名对应语法树节点的类型。ESlint在遍历语法树节点时,会执行该节点类型名所对应的回调函数。

比如上述的callExpression对应的是语法树中函数调用语句,所以当ESlint每次遍历到函数调用语句,就会执行这句callExpression回调函数。

回调函数接受一个含有当前语句的所有信息的节点对象,我们根据这些信息来判断当前语句是否非法。通过context.report来抛出代码异常,传入node和message告诉ESlint代码的位置和代码错误信息:

  1. module.exports = {
  2. create: function (context) {
  3. return {
  4. CallExpression: function (node) {
  5. // 如果函数名为alert,则报错
  6. if (node.callee.name === 'alert') {
  7. context.report({
  8. node: node,
  9. message: 'unexpected alert'
  10. });
  11. }
  12. }
  13. };
  14. }
  15. };

了解基本用法后,我们开始尝试实现简单的规则。

例子:switch必须要有default case

一般来说,我们会推荐switch语句涵盖所有情况的处理。为了避免switch语句遗漏default case的情况,我们通过ESlint自定义规则来进行检查。

首先列出非法代码:

  1. switch (name) {
  2. case 'lxj':
  3. break;
  4. }

由于所有代码语句都会解析为语法树,我们需要找到非法代码在语法树上的特征。

我们用AST explore工具来解析非法代码的结构图:

接着列出合法代码,并对比查看其特征:

  1. switch (name) {
  2. case 'lxj':
  3. break;
  4. default:
  5. }

我们能发现:

最后将这些特性判断转化成代码:

  1. module.exports = {
  2. create: function(context) {
  3. return {
  4. // 处理SwitchStatement类型语句
  5. SwitchStatement: function (node) {
  6. // 判断是否存在default case
  7. var hasDefaultCase = node.cases.some(function (caseNode) {
  8. return caseNode.test === null;
  9. });
  10. // 不存在default case则报错
  11. if (!hasDefaultCase) {
  12. context.report({
  13. node: node,
  14. message: "switch statement expect a 'default' case"
  15. });
  16. }
  17. }
  18. };
  19. }
  20. };

ESlint自带有default case的验证器,还支持注释占位功能,可以参考下官方写法美如画

综上,编写ESlint规则的基本步骤如下:

  1. 分别列出合法与非法的代码
  2. 在抽象语法树中找出非法代码特征
  3. 将这些特征用JS代码描述出来

例子:属性选择器

不管querySelector还是jQuery,非法的属性选择器都会报错。比如[name=^&*]会报错, 正确的写法应该是[name="^&*"],这种问题在开发的时候都会觉察出来并能及时改正

但是,如果涉及到字符串和变量的拼接,开发过程无法发现这一风险,报错可能发生在代码发布之后:

  1. // bad
  2. document.querySelector('[name=' + value +']');
  3. // good
  4. document.querySelector('[name="' + value +'"]');

我们用ESlint自定义规则来避免这种情况。首先列出合法和非法代码:

  1. // 合法
  2. '[name="' + value +']';
  3. // 非法
  4. '[name=' + value +']';
  5. '.class' + '[name=' + value + name +'] p';

挑选最后一句比较复杂的语句——往往复杂的语句更能体现出非法代码的特性,查看它的结构:

我们能发现,字符串结合的语句类型为BinaryExpression,其中分为左(left)和右(right)节点。右节点总是当前结合式最右边的一个元素,如果结合式内嵌了多个结合式,那么左节点也会是结合式:

结合式 = 左节点 + 右节点 = 内嵌结合式 + 最右边的结合元素

根据这个规律派生下去,直到左节点不为BinaryExpression,形成一个树状结构:

根据这个结构,我们总结出非法代码的特征:左边形如[name=,中间有若干变量,右边形如],则判断为非法代码。

接下来先准备两个函数,一个匹配[name=情况,一个匹配]情况:

  1. // 匹配形如‘[name=’
  2. function hasLeftBracket (node) {
  3. return node.type === 'Literal' && typeof node.value === 'string' &&
  4. node.value.match(/\[[^"'=]+=[^\]]*$/);
  5. }
  6. // 匹配形如‘]’
  7. function hasRightBracket (node) {
  8. return node.type === 'Literal' && typeof node.value === 'string' &&
  9. node.value.replace(/\[.*?]/g, '').match(/]/);
  10. }

字符串节点的特点:类型(type)为Literal,value属性值为字符串类型。

接下来我们定好规则的大致框架,要注意的是,由于结合式里面内嵌结合式,内嵌的结合式也会触发BinaryExpression回调函数,所以我们过滤掉内嵌的结合式,只处理最顶层的结合式:

  1. module.exports = {
  2. create: function(context) {
  3. return {
  4. BinaryExpression: function (node) {
  5. // 只处理顶级的结合式
  6. if (node.parent && node.parent.type === 'BinaryExpression') {
  7. return;
  8. }
  9. }
  10. };
  11. }
  12. };

由于内嵌结合式形成树状结构,我们通过循环来遍历左节点:

  1. while (node && node.type === 'BinaryExpression') {
  2. // ...
  3. node = node.left;
  4. }

非法代码特征要同时满足以下特征

  1. 右边有字符串满足hasLeftBracket函数,形如']'
  2. 中间有若未知变量的拼接,变量类型为Identifier
  3. 左边有字符串满足hasRightBracket函数,形如'[name='

转换为代码语言:

  1. var matchRight, matchVariable, matchLeft;
  2. while (node && node.type === 'BinaryExpression') {
  3. // 1. 右边形如']'
  4. if (!matchRight) {
  5. matchRight = hasRightBracket(node.right);
  6. }
  7. // 2. 中间若干变量
  8. if (!matchVariable) {
  9. matchVariable = node.right.type === 'Identifier';
  10. }
  11. if (matchRight && matchVariable) {
  12. // 3. 左边形如'[name='
  13. matchLeft = hasLeftBracket(node.right) || hasLeftBracket(node.left);
  14. if (matchLeft) {
  15. context.report({
  16. node: node,
  17. message: "The variable in attribute selector should wrap in quote"
  18. });
  19. return;
  20. }
  21. }
  22. node = node.left;
  23. }

综上,自定义规则的本质就是找到非法代码的特征,将合法代码和非法代码区分开来。

测试

每个ESlint规则都要配备一套单元测试,目录放置也有考究,如果自定义规则文件在 lib/rules/switch-expect-default.js,那么测试文件要放在tests/lib/rules/switch-expect-default.js

测试中必须同时提供合法代码和非法代码。以上面的switch为例,合法代码是具有default case的情况,而非法代码是没有default case的情况:

  1. const rule = require('../rules/detect-switch-default');
  2. const RuleTester = require('eslint').RuleTester;
  3. const ruleTester = new RuleTester();
  4. ruleTester.run('detect-switch-default', rule, {
  5. valid: [
  6. `switch(name) {
  7. case 'lxj':
  8. break;
  9. default:
  10. }`
  11. ],
  12. invalid: [
  13. {
  14. code: `
  15. switch(name) {
  16. case 'lxj':
  17. break;
  18. }
  19. `,
  20. errors: [{
  21. message: "switch statement expect a 'default' case",
  22. type: 'SwitchStatement'
  23. }]
  24. }
  25. ]
  26. });

我们使用mocha来测试代码,首先安装mocha:

  1. npm install mocha -D

在npm scripts里面配置测试命令,在package.json里配置:

  1. {
  2. // ...
  3. "scripts": {
  4. "test": "mocha --reporter dot tests/"
  5. }
  6. // ...
  7. }

--reporter dot 为了提高测试输出结果的可读性,可以省略。

这样,我们可以在命令行输入以下命令执行测试:

  1. npm run test

调试

我们编写的ESlint规则往往需要多次调试和修改才能通过测试。

为了方便调试,也为了能够跟踪到具体哪句代码出错或者不符合预期,我们使用iron-node来调试代码。iron-node和node-inspector一样使用Chrome的调试界面来调试nodejs。

node-inspector不稳定,不推荐使用

安装iron-node:

  1. npm install -g iron-node

安装过程耗时可能比较久,请耐心等待。iron-node安装好,我们可以调试一些普通的nodeJS代码,比如代码文件test.js:

  1. iron-node test.js

但调试devDependences需要一点技巧,官方文档有详细介绍。为了调试mocha,我们需要在package.json里加多一句命令:

  1. {
  2. // ...
  3. "scripts": {
  4. "test": "mocha --reporter dot tests/",
  5. "debug": "iron-node node_modules/mocha/bin/_mocha --reporter dot tests/"
  6. }
  7. // ...
  8. }

在规则代码需要调试的地方加入debugger;语句,然后运行以下命令调试:

  1. npm run debug

更多阅读

ESlint - working with rules

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注