之前写过一篇 eslint入门教程,只是针对一些 eslint rule 搭建以及实现一个简单的 demo,在实际应用中需要更多的 api 以及一些技巧来实现复杂的需求场景,本文针对 eslint rule 的一些开发技巧进行整理,希望对你有所帮助。

基础格式

一个 eslint 规则的基础结构如下,包含 meta 对象和 create 方法。meta 对象包含规则的元数据,create 函数返回一个对象,该对象具有 eslint 调用的方法,可以用来遍历 AST  visit 节点。

// customRule.js
module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "Description of the rule",
    },
    fixable: "code",
    schema: [], // 没有提供选项
  },
  create: function (context) {
    return {
      // 回调函数
    };
  },
};

meta元数据

meta 对象包含以下属性,分别是:

  • type:(string)表示规则的类型,枚举值:"problem""suggestion""layout" 之一。
    • problem:该规则识别将导致的错误或者可能导致的混乱行为代码,开发人员应该高度优先解决。
    • suggestion:该规则建议一些更好的代码实现方式,开发人员就算不适用该规则也不会有影响。
    • layout:该规则关心代码的格式化,比如空格,逗号,分号等。
  • docs:(object)核心规则必须要声明,自定义规则不需要太注重这个配置。
    • description:(string)规则的描述。
    • recommended:(boolean)表示在配置文件中,是否使用 "extends": "eslint:recommended" 启用该规则。
    • url:(string)在编辑器报错里,会提供一个链接给开发人员查看具体的规则文档。
  • fixable:(stirng)是否可修复,枚举值: "code""whitespace"。如果规则不可以通过命令 --fix 修复,则不需要声明。
  • hasSuggestions:(boolean)规则是否可以返回建议,不配置则默认是 false
  • schema:(object | array)规则配置,在 .eslintrc 配置的配置格式,通过 schema 来限制。
  • deprecated:(boolean)表示规则是否被废弃。
  • replaceBy:(array) 如果规则被废弃,使用替代的规则。

create方法

create 方法返回一个对象,可以方便我们遍历 AST 节点。遍历的方式可以是节点类型或者选择器 。返回的对象的可以是以下属性:

  • 节点或者选择器,在 down tree 调用。
ReturnStatement: function(node) {
  // 向下遍历调用
}
  • 节点或者选择器加 :exit ,在 up tree 调用。
"FunctionExpression:exit": function(node) {
  // 向上遍历调用
}
  • 事件名称,调用 handle 函数进行代码链路分析
onCodePathStart: function (codePath, node) {
  // 分析代码路径开始
}

选择器遍历

eslint 提供了类似css选择器的语法来方便我们匹配对应的节点类型,使得我们遍历AST节点更加方便,目前支持的选择器有:

  • AST 节点类型:ForStatement
  • 通配符(匹配所有节点):*
  • 存在属性:[attr]
  • 属性值:[attr="foo"][attr=123]
  • 属性正则:[attr=/foo.*/] (有一些已知问题)
  • 属性条件:[attr!="foo"][attr>2][attr<3][attr>=2][attr<=3]
  • 嵌套属性:[attr.level2="foo”]
  • 字段:FunctionDeclaration > Identifier.id
  • 第一个或最后一个子项::first-child:last-child
  • 第 n 个子项(不支持 ax+b)::nth-child(2)
  • 最后 n 个子项(不支持 ax+b)::nth-last-child(1)
  • 后裔:FunctionExpression ReturnStatement
  • 子项:UnaryExpression > Literal
  • 随同兄弟:VariableDeclaration ~ VariableDeclaration
  • 相邻兄弟:ArrayExpression > Literal + SpreadElement
  • 否定::not(ForStatement)
  • 任意匹配::matches([attr] > :first-child, :last-child)
  • AST 节点类::statement:expression:declaration:function:pattern

例如我想实现一个禁止使用 window.open ,有两种写法:

不用选择器

create(context) {
  return {
    MemberExpression(node) {
      if (
        node.object.type === "Identifier" &&
        node.object.name === "window" &&
        node.property.type === "Identifier" &&
        node.property.name === "open"
      ) {
        context.report({
          node,
          messageId: "tip",
        });
      }
    },
  };
}

使用选择器

create(context) {
  return {
    "MemberExpression[property.name='open'][object.name='window']": (node) => {
      context.report({
        node,
        messageId: "tip",
      });
    },
  };
},

可以看出使用选择器的语法会更清晰,方便开发。

上下文对象

create 方法提供了一个参数 context 对象,可以让我们获取到当前上下文一些信息,具体可以查阅官方文档,上下文对象

我们重点关注 context.report() 方法,eslint 就是通过该方法给开发人员报告错误信息。

context.report({
  node: node,
  message: "Unexpected identifier"
});

message 可以使用占位符的方式定义,在 meta 配置 messages 对象,就可以方便 message 复用了。

module.exports = {
  meta: {
    messages: {
      avoidName: "Avoid using variables named '{{ name }}'",
    },
  },
  create(context) {
    return {
      Identifier(node) {
        if (node.name === "foo") {
          context.report({
            node,
            messageId: "avoidName",
            data: {
              name: "foo",
            },
          });
        }
      },
    };
  },
};