ESLint 是 js 代码格式化工具,能够自动发现并尝试修复代码中的问题,是团队开发的必备工具。大部分时候,使用既定规则即可,业界也有比较多的成熟配置:

  • eslint:recommended ESLint内置的推荐规则在么有讲到 所有打钩的就是内置规则
  • eslint:all:ESLint 内置的所有规则;
  • eslint-config-standard:standard 的 JS 规范;
  • eslint-config-prettier:关闭和 ESLint 中以及其他扩展中有冲突的规则;
  • eslint-config-airbnb-base:airbab 的 JS 规范;
  • eslint-config-alloy:腾讯 AlloyTeam 前端团队出品,可以很好的针对你项目的技术栈进行配置选择,比如可以选 React、Vue(现已支持 Vue 3.0)、TypeScript 等;

本篇并不想讨论这些规则的配置和使用,搜索引擎学习这些内容会更加容易。

问题

var date = new Date("2023-02-16 06:00");

上面的代码在pc上没有大问题,但是在移动端 ios 中会报错,ios 不支持 2023-02-16 06:00 这样的写法,需要写成 2023/02/16 06:00 这样的形式。

在项目管理中,团队人员多了,很难从口头成面达成一致,最好还是从 ESLint 规则上面去想办法。

源码

ESLint 第一个 commit 是 2013 年,2013.7.1 发布了 0.0.2 版本,后续虽然一直在更新各种功能,但是核心逻辑其实没有变化,我们看 0.0.2 版本的源码会更加轻松一点。

我们熟悉的 no-console 等规则在第一版已经实现。我们看一下它的具体实现

module.exports = function(context) {
    return {
        "MemberExpression": function(node) {
            if (node.object.name === "console") {
                context.report(node, "Unexpected console statement.");
            }
        }
    };
};

代码很简单,判断节点是否是 console,如果是,那么调用 context.report 函数去上报异常。MemberExpression 这个方法名称有点奇怪,可能是比较核心的内容。

我们继续梳理代码,很快就会发现这一块内容

逻辑非常清晰,读取配置,读取规则,对文件应用规则。
继续跟踪读取文件:

jscheck 是一个类,上面挂载了一些方法,verify 似乎是干活的主要部分:

部分配置项被我收起,不影响我们核心理解,这里出现了两个 npm 包:

esprima

var esprima = require('esprima');
var program = 'const answer = 42';
var program2 = 'console.log(123)';

console.log(JSON.stringify(esprima.parse(program)))
console.log(JSON.stringify(esprima.parse(program2)))

输出:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "answer"
          },
          "init": {
            "type": "Literal",
            "value": 42,
            "raw": "42"
          }
        }
      ],
      "kind": "const"
    }
  ],
  "sourceType": "script"
}
{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "CallExpression",
        "callee": {
          "type": "MemberExpression",
          "computed": false,
          "object": {
            "type": "Identifier",
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "name": "log"
          }
        },
        "arguments": [
          {
            "type": "Literal",
            "value": 123,
            "raw": "123"
          }
        ]
      }
    }
  ],
  "sourceType": "script"
}

我们前面遇到的 MemberExpression 在这里再次出现,成员表达式节点,即表示引用对象成员的语句。这里是把 js 转换成抽象语法树,后续根据语法树去判读是否和规则匹配。

常见的 AST 节点:

  • Identifier
  • ImportSpecifier
  • ImportDefaultSpecifier
  • CallExpression
  • MemberExpression
  • AssignmentExpression
  • ArrayExpression
  • VariableDeclaration
  • LogicalExpression
  • ConditionalExpression
  • IfStatement
  • Literal
  • BinaryExpression
  • BinaryOperator
  • ExpressionStatement
  • ThisExpression
  • ObjectExpression
  • NewExpression
  • Property
  • TemplateLiteral
  • TemplateElement

常见的节点比较多,不需要死记硬背,有个大致印象就好。需要生成 ast ,可以使用下面这个网站:
https://astexplorer.net/

可以快速生成查阅 ast 节点。

astw

esprima 是生成 ast,那么 astw 猜测这里就是遍历 ast ?

var ast = esprima.parse(text, { loc: true, range: true }),
            walk = astw(ast);

        walk(function(node) {
            api.emit(node.type, node);
        });

第一次调用,也是做的一些配置合并的操作,返回了 walk 函数。
walk 函数再次调用,会遍历 ast 节点,对于最终子节点,会执行回调函数。
回调函数根据事件 emit、on 进一步执行是否需要 report 的逻辑。

到这里,源码分析基本完成。逻辑非常清晰,读取配置,读取自定义规则,配置合并,文件转换为 ast,遍历 ast 节点并和规则做对比。

后续 ESLint 迭代

后续 ESLint 越来越复杂,部分开源的内容,已经跟不上 ESLint 的发展,有了一些自己的视线

plugin 机制,你可以直接替换编译器,自己构建语法解析器,ESLint 完全变成工具人,只负责主线流程,其他都可以自己实现,使得社区快速完成了对 vue、jsx 等的语法解析。直接干死了 jslint。后续 ts 推出了 TSLint,但是没多久也放弃了,直接被 ESLint 吞并。

跑题了?

咦,我们是不是跑题了?看到这里,我还不会写 ESLint 规则嘛。下面其实很简单了。

全局安装脚手架

npm i -g yo
npm i -g generator-eslint

生成项目骨架

yo eslint:plugin

根据这个骨架,你就可以开发自己的 ESLint 规则,比如 new Date 兼容性问题核心代码如下:

NewExpression(node){
    if(node.callee.name === 'Date'){
        if (node.arguments.length && node.arguments[0].value && node.arguments[0].value.includes("-")){
            context.report({
                node,
                message: "new Date('2022-01-01') 横线写法在ios中会报错",
            });
        }
    }
}

更多实现细节请参考我对支付宝小程序写的规则
eslint-plugin-alipay-minihttps://github.com/Yaob1990/eslint-plugin-alipay-mini

参考资料

前端 - 13 个示例快速入门 JS 抽象语法树 - 不挑食的程序员 - SegmentFault 思否