如何写自定义的ESLint规则 (Customized ESLint Rule)

背景


ESLint 是一个很好用的 js 静态代码检查器,通过在编译环境(IDE)安装插件,可以时时的对 js 代码进行代码风格的校验以及常见的js代码错误。引入 ESLint 可以帮助我们检查编程规范从而提高代码的质量。关于 ESLint 的入门介绍网上有很多很好的资源,这里就不再赘述。

面对的问题


由于项目中有许多结合项目本身而自定义的编程规范,但对于一个人员流动较大、开发人员较多的团队通过口口相传和 code review 来执行编程规范显然是事倍功半的。这时就需要利用 ESLint 来 DIY 我们自己的规则。

小试牛刀


我们先来实现一个简单的自定义的 ESLint 规则。举个例子,我们不允许任何标识符 (identifier) 中出现 hello 这个单词。

我们先创建一个新项目然后创建一个叫eslint_rules的文件夹,然后创建一个文件叫做 no-hello-in-identifier 的 js 文件,然后 no-hello-in-identifier.js 代码如下:

var _ = require('lodash')

module.exports = {
  meta: {
    messages: {
      invalidName: 'Avoid use \'hello\' for identifier'
    }
  },
  create (context) {
    return {
      Identifier (node) {
        if (_.includes(node.name, 'hello')) {
          context.report({
            node,
            messageId: 'invalidName'
          })
        }
      }
    }
  }
}

现在我们已经定义了一个属于自己的规则,在这个规则中:

  • meta 用于定义这个规则的一些元数据(metadata)比如示例中的 messages 就用于当检测到某代码违反此规则时显示的错误信息。meta 里面还可设置更多的元数据,之后会讲到。
  • create 方法返回一个对象,这个对象中包含着一些方法,这些方法用于在 ESLint 遍历 JS 代码的抽象语法树 (AST)时被调用。create 带有一个叫 context 的参数,它是一个对象,这个对象提供了一些非常有用的方法来帮助我们实现规则。
  • crate 中的方法由两部分组成:选择器 selector 和 访问器 visitor。示例中的 identifier 便是一个选择器,简而言之,选择器就是用来过滤我们想要的语法树节点 (node)。比如我想要对所有的标识符节点做处理就用 Identifier 作为选择器。更多关于选择器的内容请参考这里。而访问器就是函数主体,也就是我们怎样定义这个规则的地方。
  • ndoe.name 通过访问节点中的 name 属性来获取节点的 name。
  • context.report 这个方法用来向用户报告错误信息。其中 node 代表当前节点,messageId 引用了我们现在 meta 中的信息。

现在问题来了,我们如何让 ESLint 去执行我们定义的规则呢?

其实很简单只需要将我们的 rule 引入到我们配置文件中,而后执行 eslint <filename> --rulesdir <route/to/rules> <filename-of-rule> 即可 (如何引入 ESLint 以及 ESLint 的常规配置请参考前文提到的 ESLint 入门链接)。简单的配置内容如下:

module.exports = {
  "extends": "eslint:recommended",
  "rules": {
    "no-hello-in-identifier": 2
  }
};

现在我们写一个简单的 index.js 然后用我们的 ESLint rule 来检测它。

function helloWorld () {
  return 'hello world!'
}
console.log(helloWorld())

当我们执行 eslint index.js --rulesdir eslint_rules/lib no-hello-in-identifier.js 后就可看到下面的结果

可以看到第一行和第四行是我们自定义的规则!现在我们自定义的规则已经开始起作用了。

到目前为止我们实现了一个简单的自定义规则,并将它成功的运用到了我们的 js 代码中。接下来我们就来实现一个我在项目中真实遇到的需求。

实战演练

在我工作的项目中,我们用引用了 reselect 来组装 redux store 中的数据,然后将组装好的数据传给 jsx component 用于渲染 Web 页面。但是在 reselect 的官网上对于 selector 的命名有两种方式:getXXXXXXXXXXSelector 于是两种命名方式在项目中同时存在。虽然这不是个大问题,但会造成新成员额外的学习成本,而且降低代码的可读性。现在我们就通过 ESLint 来使所有的 selector 都按照 getXXXXX 的格式来命名
(注意:这里的 selector 指的是用于组装 redux store 数据的 selector,而不是前文中指的 ESLint 的 selector)

在开始 coding 之前,我们现在 tasking 一下我们需要做的事情。

  • 通过 meta 设置我们想要输出的错误信息
  • 过滤不必要的文件
  • 找到合适的 ESLint selector(注意区分用于组装 redux store 数据的 selector)
  • 实现 visitor

现在我们就来一步一步实现我们的规则。

通过 meta 设置我们想要输出的错误信息

这一步很简单,与之前的例子大致相同。以下是代码:

module.exports = {
  meta: {
    messages: {
      invalidName: `Avoid using variables named, use 'getXXXXX' to name a selector`
    }
  },
  create(context) {
    //filer files code
    return {
      // customized rules
    };
  }
};

过滤不必要的文件

在我们的 code base 中所有的 selector 都会定义在文件名以 selectorselectors 结尾的文件。所以需要过滤掉其他文件以避免我们的规则出现“误杀”。这实现起来也很简单。
context 提供了一个叫 getFilename 的方法,可以帮助我们直接通过这个方法拿到当前的文件名,于是就可以过滤掉不需要校验的文件。代码如下:

const fileNameRegExp = /.*Selectors?.js$/;

module.exports = {
  meta: {
    messages: {
      invalidName: `Avoid using variables named, use 'getXXXXX' to name a selector`
    }
  },
  create(context) {
    if(!fileNameRegExp.test(context.getFilename())) return {};
    return {
      // customized rules
    };
  }
};

找到合适的 ESLint selector

那在这个文件下,什么要得节点才是我们想要的节点呢?首先,我们所有的 selector 都会暴露出去,所以这个节点必然在 export 或者 export default 之中,而通过 AST 的在线编译工具我们不难发现这两个关键字分别在 ExportNamedDeclarationExportDefaultDeclaration 之下。然而仅仅使用这两个是不行的,通过调用 node.name 我们发现我们拿到的并不是我们想要的变量名而是 undefinded 这是因为在语法树中这两个关键字下并没有 name 这个属性。但是,我们通过官方文档我们发现可以通过下面的方式来找到我们想要的变量名:
'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > Identifier'
找到了合适的 selector, 就可以继续改进我们的代码:

const fileNameRegExp = /.*Selectors?.js$/;

module.exports = {
  meta: {
    messages: {
      invalidName: `Avoid using variables named, use 'getXXXXX' to name a selector`
    }
  },
  create(context) {
    if(!fileNameRegExp.test(context.getFilename())) return {};
    return {
      'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > Identifier': (node) => {
        checkSelectorName(node, context);
      }
    };
  }
};

目前我们只定义了一条规则,它检测了所有位于 ExportNamedDeclaration 下的 VariableDeclaration 下的 VariableDeclaratorIdentifier 的变量名是否含有 hello(这里的 checkSelectorName() 我们之后会实现它)
但我们还需要检测所有位于 ExportDefaultDeclaration 下的 Identifier, 所有我们需要加一条规则。方法很简单,在返回的对象中再添加一个 selector 和 visitor 即可:

const fileNameRegExp = /.*Selectors?.js$/;

module.exports = {
  meta: {
    messages: {
      invalidName: `Avoid using variables named, use 'getXXXXX' to name a selector`
    }
  },
  create(context) {
    if(!fileNameRegExp.test(context.getFilename())) return {};
    return {
      'ExportNamedDeclaration > VariableDeclaration > VariableDeclarator > Identifier': (node) => {
        checkSelectorName(node, context);
      },
      'ExportDefaultDeclaration > Identifier': (node) => {
        checkSelectorName(node, context);
      }
    };
  }
};

这样整体框架就已经完成了,现在我们只需要实现 checkSelectorName() 也就是我们的 visitor 就可以了。

实现 visitor

这个检测很简单,这要检测我们的变量名是否满足正则,不满足则向用户发送错误报告。

const selectorRegExp = /^(?:(get)|(is)|(should)|(has))/;
const checkSelectorName = (node, context) => {
  const nodeName = node.name;
  if(!selectorRegExp.test(nodeName)) {
    context.report({
      node,
      messageId: 'invalidName',
      data: {
        name: node.name
      }
    });
  }
};

这样整个自定义的 ESLint 就完成了!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • ESLint 配置 ESlint 被设计为完全可配置的,这意味着你可以关闭每一个规则而只运行基本语法验证,或混合和...
    静默虚空阅读 40,998评论 3 14
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,037评论 1 32
  • 简介浏览器可以被认为是使用最广泛的软件,本文将介绍浏览器的工 作原理,我们将看到,从你在地址栏输入google.c...
    听风阁阅读 3,203评论 0 7
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,027评论 8 265
  • 自私,是指爱自己的东西胜过他人。 1,本分 2,利己 3,自私 4,贪图 5,贪婪 6,损人 多纬度解析 本分 生...
    南风窗口阅读 440评论 0 1