前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >eslint-v0.0.2做了什么

eslint-v0.0.2做了什么

作者头像
windliang
发布2022-09-23 13:13:28
5660
发布2022-09-23 13:13:28
举报
文章被收录于专栏:windliang的博客windliang的博客

准备了解一下 eslint 的原理,就先看一下最早一版 eslint 的实现吧。github 打了 tag 的最早的版本就是 0.0.2 了,提交记录是八年前了。

git clone git@github.com:eslint/eslint.git 并且 git checkout v0.0.2 ,先看一下 package.json

代码语言:javascript
复制
{
  "name": "jscheck",
  "version": "0.0.2",
  "author": "Nicholas C. Zakas <nicholas+npm@nczconsulting.com>",
  "description": "An AST-based pattern checker for JavaScript.",
  "main": "./lib/jscheck.js",
  "bin": {
    "jscheck": "./bin/jscheck.js"
  },
  "scripts": {
    "ctest": "istanbul cover --print both vows -- --spec ./tests/*/*/*.js",
    "test": "vows -- --spec ./tests/*/*/*.js"
  },
  "repository": "",
  "dependencies": {
    "optimist": "*",
    "astw": "*",
    "esprima": "*"
  },
  "devDependencies": {
    "vows": "~0.7.0",
    "istanbul": "~0.1.10",
    "sinon": "*"
  },
  "keywords": [
    "ast",
    "lint",
    "javascript",
    "ecmascript"
  ],
  "preferGlobal": true,
  "license": "BSD"
}

主要涉及到 optimistastwesprima ,我们来依次了解一下。

optimist

主要作用就是帮我们解析命令行参数,我们来试验一下。

在根目录新建一个 cli.js ,并且赋予执行权限,执行 chmod +x ./cli.js ,输入下边的内容:

代码语言:javascript
复制
#!/usr/bin/env node
var optimist = require("optimist");
console.log('argv 收到的参数')
console.log(process.argv);
console.log('optimist 解析后的参数')
console.log(optimist.parse(process.argv.slice(2)));

#!/usr/bin/env node 指明使用 node 执行当前脚本,就可以直接使用 ./cli.js 执行命令,而不需要使用 node ./cli.js 执行。

processnode 为我们提供的一个全局变量,可以拿到命令行参数 argv

然后执行 ./cli.js -w --hello 23 --no-ugly --name=test ./fils.js ./file2.js,控制台会输出如下:

代码语言:javascript
复制
argv 收到的参数
[
  '/Users/wangliang/.nvm/versions/node/v14.17.3/bin/node',
  '/Users/wangliang/windliang/eslint/cli.js',
  '-w',
  '--hello',
  '23',
  '--no-ugly',
  '--name=test',
  './fils.js',
  './file2.js'
]
optimist 解析后的参数
{
  _: [ './fils.js', './file2.js' ],
  w: true,
  hello: 23,
  ugly: false,
  name: 'test',
  '$0': '../../.nvm/versions/node/v14.17.3/bin/node ./cli.js'
}

可以看到 argv[0]node 的路径,argv[1] 是要执行脚本的路径,从 argv[2] 开始是我们要的参数,所以代码里我们执行了 argv.slice(2)

通过 optimist 解析,我们就可以得到相应的 keyvalue 键值对了。

esprima

可以做词法分析或者生成 AST 的语法树,直接看示例。

代码语言:javascript
复制
#!/usr/bin/env node

var esprima = require("esprima");
var program = `const answer = 42;
if(answer == 5){console.log(answer)}
`;

console.log(`词法分析`);
console.log(esprima.tokenize(program));

console.log(`AST 语法树`);
console.log(JSON.stringify(esprima.parseScript(program), null, 2));

看一下输出:

代码语言:javascript
复制
词法分析
[
  { type: 'Keyword', value: 'const' },
  { type: 'Identifier', value: 'answer' },
  { type: 'Punctuator', value: '=' },
  { type: 'Numeric', value: '42' },
  { type: 'Punctuator', value: ';' },
  { type: 'Keyword', value: 'if' },
  { type: 'Punctuator', value: '(' },
  { type: 'Identifier', value: 'answer' },
  { type: 'Punctuator', value: '==' },
  { type: 'Numeric', value: '5' },
  { type: 'Punctuator', value: ')' },
  { type: 'Punctuator', value: '{' },
  { type: 'Identifier', value: 'console' },
  { type: 'Punctuator', value: '.' },
  { type: 'Identifier', value: 'log' },
  { type: 'Punctuator', value: '(' },
  { type: 'Identifier', value: 'answer' },
  { type: 'Punctuator', value: ')' },
  { type: 'Punctuator', value: '}' }
]
AST 语法树
{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "answer"
          },
          "init": {
            "type": "Literal",
            "value": 42,
            "raw": "42"
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "IfStatement",
      "test": {
        "type": "BinaryExpression",
        "operator": "==",
        "left": {
          "type": "Identifier",
          "name": "answer"
        },
        "right": {
          "type": "Literal",
          "value": 5,
          "raw": "5"
        }
      },
      "consequent": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ExpressionStatement",
            "expression": {
              "type": "CallExpression",
              "callee": {
                "type": "MemberExpression",
                "computed": false,
                "object": {
                  "type": "Identifier",
                  "name": "console"
                },
                "property": {
                  "type": "Identifier",
                  "name": "log"
                }
              },
              "arguments": [
                {
                  "type": "Identifier",
                  "name": "answer"
                }
              ]
            }
          }
        ]
      },
      "alternate": null
    }
  ],
  "sourceType": "script"
}

此外,解析 Ast 语法树的时候为我们提供了 range 参数和 loc 参数,esprima.parseScript(program, { loc: true, range: true }),输出节点的时候可以帮我们输出源代码的位置,类似于下边的样子。

代码语言:javascript
复制
"type": "VariableDeclarator",
  "id": {
    "type": "Identifier",
      "name": "answer",
        "range": [
          6,
          12
        ],
          "loc": {
            "start": {
              "line": 1,
                "column": 6
            },
              "end": {
                "line": 1,
                  "column": 12
              }
          }
  },

astw

ast walk,输入源代码或者 AST 对象,然后调用 walk 方法传入回调,会帮我们依次遍历 ast 的节点,同样看个例子就明白了。

为了更好的看出输出的结果,我们引入 escodegen 库,可以将遍历的 ast 节点还原为源代码。

代码语言:javascript
复制
#!/usr/bin/env node

var astw = require("astw");
var esprima = require("esprima");
var program = `const answer = 42;
if(answer == 5){console.log(answer)}
`;

console.log(JSON.stringify(esprima.parseScript(program), null, 2));
var walk = astw(program);
var deparse = require("escodegen").generate;
let count = 1;
walk(function (node) {
  var src = deparse(node);
  console.log(count++, node.type + " :: " + JSON.stringify(src));
});

看一下结果:

代码语言:javascript
复制
{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "answer"
          },
          "init": {
            "type": "Literal",
            "value": 42,
            "raw": "42"
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "IfStatement",
      "test": {
        "type": "BinaryExpression",
        "operator": "==",
        "left": {
          "type": "Identifier",
          "name": "answer"
        },
        "right": {
          "type": "Literal",
          "value": 5,
          "raw": "5"
        }
      },
      "consequent": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ExpressionStatement",
            "expression": {
              "type": "CallExpression",
              "callee": {
                "type": "MemberExpression",
                "computed": false,
                "object": {
                  "type": "Identifier",
                  "name": "console"
                },
                "property": {
                  "type": "Identifier",
                  "name": "log"
                }
              },
              "arguments": [
                {
                  "type": "Identifier",
                  "name": "answer"
                }
              ]
            }
          }
        ]
      },
      "alternate": null
    }
  ],
  "sourceType": "script"
}
1 Identifier :: "answer"
2 Literal :: "42"
3 VariableDeclarator :: "answer = 42"
4 VariableDeclaration :: "const answer = 42;"
5 Identifier :: "answer"
6 Literal :: "5"
7 BinaryExpression :: "answer == 5"
8 Identifier :: "console"
9 Identifier :: "log"
10 MemberExpression :: "console.log"
11 Identifier :: "answer"
12 CallExpression :: "console.log(answer)"
13 ExpressionStatement :: "console.log(answer);"
14 BlockStatement :: "{\n    console.log(answer);\n}"
15 IfStatement :: "if (answer == 5) {\n    console.log(answer);\n}"
16 Program :: "const answer = 42;\nif (answer == 5) {\n    console.log(answer);\n}"

可以看到 walk 方法会帮助我们从内到外的遍历 AST 的节点,通过回调将当前节点返回。

原理

知道了 AST 树,我们其实就可以实现最简单的 Eslint 检查了,比如最常见的是否使用了 ===

举个例子,对于 answer == 42; 我们在 walk 过程中会得到这样一个节点。

代码语言:javascript
复制
Node {
  type: 'BinaryExpression',
  start: 22,
  end: 33,
  left: Node {
    type: 'Identifier',
    start: 22,
    end: 28,
    name: 'answer',
    parent: [Circular *1]
  },
  operator: '==',
  right: Node {
    type: 'Literal',
    start: 32,
    end: 33,
    value: 5,
    raw: '5',
    parent: [Circular *1]
  },
  parent: Node {
    type: 'IfStatement',
    start: 19,
    end: 55,
    test: [Circular *1],
    consequent: Node { type: 'BlockStatement', start: 34, end: 55, body: [Array] },
    alternate: null,
    parent: Node {
      type: 'Program',
      start: 0,
      end: 56,
      body: [Array],
      sourceType: 'script'
    }
  }
}

根据这个 ast 的节点,首先判断 type 是不是 BinaryExpression,然后再判断 operator 是否是 ==!= 就可以了。

代码语言:javascript
复制
if(node.type === 'BinaryExpression'){
  if (operator === "==") {
   输出(node, "Unexpected use of ==, use === instead.");
  } else if (operator === "!=") {
    输出(node, "Unexpected use of !=, use !== instead.");
  }
}

对于单一的规则很好实现,但把多个规则整合起来,并且便于用户扩展就是个学问了,这里学习一下 eslint 是怎么整合的。

EventEmitter 库

一个 Ast 节点对应一个要处理的规则,每遍历一个节点,就去处理相应的规则。这里使用了订阅/发布的设计模式,node.js 提供了 events.EventEmitter 库供我们使用。

我们只需要遍历所有规则列表,然后调用 on 方法,订阅相关事件,事件名就是 node.type,比如上边介绍的 BinaryExpression

代码语言:javascript
复制
var EventEmitter = require("events").EventEmitter;
...
var api = Object.create(new EventEmitter()),
...

Object.keys(config.rules).forEach(function(key) {

  var ruleCreator = rules.get(key),
      rule;

  if (ruleCreator) {
    rule = ruleCreator(new RuleContext(key, api));

    // add all the node types as listeners
    Object.keys(rule).forEach(function(nodeType) {
      api.on(nodeType, rule[nodeType]);
    });
  } else {
    throw new Error("Definition for rule '" + key + "' was not found.");
  }
});

然后在调用 astw 库的 walk 方法的时候 emit 一下 node.type 事件名即可。

代码语言:javascript
复制
var ast = esprima.parse(text, { loc: true, range: true }),
    walk = astw(ast);

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

源码分析

先看一下代码目录:

代码语言:javascript
复制
eslint
├── LICENSE
├── README.md
├── bin
│   └── jscheck.js //入口文件,调用 cli.js 的 execute
├── config
│   └── jscheck.json //eslint 配置文件,定义检测哪些规则
├── lib
│   ├── cli.js // 主函数
│   ├── jscheck.js // 提供 verify 方法
│   ├── reporters
│   │   └── compact.js // 格式化输出的内容
│   ├── rule-context.js // 将 jsCheack 对象的方法提过给 rule 调用
│   ├── rules // 预制的规则
│   │   ├── camelcase.js
│   │   ├── curly.js
│   │   ├── eqeqeq.js
│   │   ├── no-bitwise.js
│   │   ├── no-console.js
│   │   ├── no-debugger.js
│   │   ├── no-empty.js
│   │   ├── no-eval.js
│   │   └── no-with.js
│   └── rules.js // 读取 rule 规则
├── package-lock.json
├── package.json
└── tests
    └── lib
        └── rules
            ├── camelcase.js
            ├── no-bitwise.js
            ├── no-debugger.js
            ├── no-eval.js
            └── no-with.js

看一下 lib/cli.js 的主逻辑:

代码语言:javascript
复制
execute: function (argv, callback) {
  var options = optimist.parse(argv),
      files = options._,
      config;
  if (options.h || options.help) {
  } else {
    config = readConfig(options);

    // TODO: Figure out correct option vs. config for this
    // load rules
    if (options.rules) { // 用户传入自定义的 rules
      rules.load(options.rules);
    }

    if (files.length) {
      processFiles(files, config);
    } else {
      console.log("No files!");
    }
  }
},

其中 readConfig 就是读取了配置文件,为用户提供了 c/config 参数。

代码语言:javascript
复制
function readConfig(options) {
  var configLocation = path.resolve(
    __dirname,
    options.c || options.config || DEFAULT_CONFIG
  );
  return require(configLocation);
}

默认的 DEFAULT_CONFIG 路径是 ../config/jscheck.json ,内容如下:

代码语言:javascript
复制
{
    "rules": {
        "no-bitwise": 1,
        "no-eval": 1,
        "no-with": 1,
        "no-empty": 1,
        "no-debugger": 1,
        "no-console": 1,

        "camelcase": 1,
        "eqeqeq": 1,
        "curly": 1
    }
}

processFiles(files, config) 主要就是两层循环,循环要检查的文件和上边的配置。

代码语言:javascript
复制
function processFiles(files, config) {
  var fullFileList = [];

  // 如果是目录的话,继续递归去添加
  files.forEach(function (file) {
    if (isDirectory(file)) {
      fullFileList = fullFileList.concat(getFiles(file));
    } else {
      fullFileList.push(file);
    }
  });

  // 遍历文件
  fullFileList.forEach(function (file) {
    processFile(file, config);
  });
}

看一下 processFile 函数。

代码语言:javascript
复制
function processFile(filename, config) {
  // 读取文件
  var text = fs.readFileSync(path.resolve(filename), "utf8"),
      // 检查文件
    messages = jscheck.verify(text, config);

  console.log(reporter(jscheck, messages, filename, config));
}

verify 就是核心逻辑了,调用了 on 事件和 emit 事件。

代码语言:javascript
复制
api.verify = function (text, config) {
    // reset
    this.removeAllListeners();
    messages = [];

    // enable appropriate rules
    Object.keys(config.rules).forEach(function (key) {
      var ruleCreator = rules.get(key),
        rule;
      if (ruleCreator) {
        // 将 js api 的 context 传给 rule
        rule = ruleCreator(new RuleContext(key, api));

        // add all the node types as listeners
        // rule 规则
        Object.keys(rule).forEach(function (nodeType) {
          api.on(nodeType, rule[nodeType]);
        });
      } else {
        throw new Error("Definition for rule '" + key + "' was not found.");
      }
    });

    // save config so rules can access as necessary
    currentConfig = config;
    currentText = text;

    /*
     * Each node has a type property. Whenever a particular type of node is found,
     * an event is fired. This allows any listeners to automatically be informed
     * that this type of node has been found and react accordingly.
     */
    var ast = esprima.parse(text, { loc: true, range: true }),
      walk = astw(ast);

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

    return messages;
  };

其中 ruleCreator 就是某个规则对应的内容比如下边的 curly.js 文件。

其中,上边的 new RuleContext(key, api) 就是生成了下边的 context,提过了 report 等其他方法。

这样用户自定义 rule 的时候,通过 context 就可以调用 eslint 暴露出来的方法。

代码语言:javascript
复制
module.exports = function (context) {
  return {
    IfStatement: function (node) {
      if (node.consequent.type !== "BlockStatement") {
        context.report(node, "Expected { after 'if' condition.");
      }

      if (node.alternate && node.alternate.type !== "BlockStatement") {
        context.report(node, "Expected { after 'else'.");
      }
    },

    WhileStatement: function (node) {
      if (node.body.type !== "BlockStatement") {
        context.report(node, "Expected { after 'while' condition.");
      }
    },

    ForStatement: function (node) {
      if (node.body.type !== "BlockStatement") {
        context.report(node, "Expected { after 'for' condition.");
      }
    },
  };
};

上边就是 eslint v0.0.2 的全部代码了,更细节的内容可以在本地 git clone git@github.com:eslint/eslint.git 并且 git checkout v0.0.2 看。

核心原理就是通过 AST 语法树来进行相应的检查,然后通过 EventEmitter 进行组织调用,使用 RuleContext 将一些方法暴露出来供 rule 使用。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-08,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 windliang 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • optimist
  • esprima
  • astw
  • 原理
  • EventEmitter 库
  • 源码分析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com