eslint原理及插件实现

137 阅读2分钟

Eslint 使用Espree 进行Javascript解析 Eslint 使用AST来评估代码中的模式 Eslint 每一条规则都是一个插件

一些解析器:

  • esprima
  • acorn
  • @babel/parse (babylon) 基于acorn
  • espree 最初从Esprima中fork出来,现在基于acorn

首先, 初始化工程,安装相应的包

pnpm add esprima estraverse escodegen

写一个小例子, 复习下之前编译原理文章里提到的ast相关知识

image.png

代码转换

需求:修改函数名称

const esprima = require("esprima");
const estraverse = require("estraverse");
const escodegen = require("escodegen");

let code = `function a(){}`;

// 将代码转成ast语法树
const ast = esprima.parseScript(code);

estraverse.traverse(ast, {
  enter(node) {
    console.log("enter", node.type);
    if (node.type === "FunctionDeclaration") {
      node.id.name = "ast";
    }
  },
  leave(node) {
    console.log("leave", node.type);
  },
});
const output = escodegen.generate(ast);
console.log(output); // function ast() {}

Babel插件

需求:转换箭头函数

pnpm add @babel/core // babel的编译器,核心api都在里面,实现了插件功能
pnpm add @babel/types // 用于ast的lodash式工具库,验证以及转换AST节点的方法
pnpm add babel-plugin-transform-es2015-arrow-functions // 转换箭头函数
const babel = require("@babel/core");
const types = require("@babel/types");
const transformFunction = require("babel-plugin-transform-es2015-arrow-functions");

const code = `const sum = () => a+b`;

const result = babel.transform(code, {
  plugins: [transformFunction],
});

console.log(result.code); 
/**
const sum = function () {
  return a + b;
};
 */

接下来,手写babel插件, transformFunction

const babel = require("@babel/core");
const types = require("@babel/types");
// const transformFunction = require("babel-plugin-transform-es2015-arrow-functions");

const transformFunction = {
  visitor: {
    ArrowFunctionExpression(path) {
      let { node } = path;
      node.type = "FunctionExpression";

      hoistFunctionEvn(path);
      let body = node.body;
      if (!types.isBlockStatement(body)) {
        node.body = types.blockStatement([types.returnStatement(body)]);
      }
    },
  },
};
function getThisPath(path) {
  let arr = [];
  path.traverse({
    ThisExpression(path) {
      arr.push(path);
    },
  });
  return arr;
}

function hoistFunctionEvn(path) {
  // 查找父级作用域
  const thisEnv = path.findParent(
    (parent) =>
      (parent.isFunction && !parent.isArrowFunctionExpression()) ||
      parent.isProgram()
  );

  const bindingThis = "_this";
  const thisPaths = getThisPath(path);
  // 修改当前路径的this,改为_this

  thisPaths.forEach((path) => {
    // this -> _this
    path.replaceWith(types.identifier(bindingThis));
  });

  thisEnv.scope.push({
    id: types.identifier(bindingThis),
    init: types.ThisExpression(),
  });
}

const code = `const sum = () => a+b`;

const result = babel.transform(code, {
  plugins: [transformFunction],
});

console.log(result.code);

编写自定义eslint插件

需求: 实现no-var, 面向api编程

sudo npm install yo generator-eslint -g
yo eslint:plugin
yo eslint:rule

/**
 * @fileoverview kk
 * @author my
 */
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: "problem", // `problem`, `suggestion`, or `layout`
    docs: {
      description: "不能有var关键字",
      recommended: false,
      url: null, // URL to the documentation page for this rule
    },
    fixable: "code", // Or `code` or `whitespace`
    schema: [], // Add a schema if the rule has options
    messages: {
      unexpected: "不能使用 {{ type }}",
    }, // Add messageId and message
  },

  create(context) {
    // variables should be defined here
    const sourceCode = context.sourceCode;

    //----------------------------------------------------------------------
    // Helpers
    //----------------------------------------------------------------------

    // any helper functions should go here or else delete this section

    //----------------------------------------------------------------------
    // Public
    //----------------------------------------------------------------------

    return {
      // visitor functions for different types of nodes
      VariableDeclaration(node) {
        if (node.kind === "var") {
          context.report({
            node,
            data: { type: "var" },
            messageId: "unexpected",
            fix(fixer) {
              const varToken = sourceCode.getFirstToken(node, {
                filter: (t) => t.value === "var",
              });
              return fixer.replaceText(varToken, "let");
            },
          });
        }
      },
    };
  },
};