Master AST Basics and Build Custom ESLint & Stylelint Plugins for JavaScript

This comprehensive guide explains abstract syntax trees (AST), demonstrates how to parse and transform JavaScript code with Acorn, shows step‑by‑step creation of custom ESLint and Stylelint plugins—including rules like no‑console and function‑to‑arrow conversion—and even covers a React live‑code example using Babel and AST analysis.

ELab Team
ELab Team
ELab Team
Master AST Basics and Build Custom ESLint & Stylelint Plugins for JavaScript

大厂技术 坚持更 精选好文

认识 AST

定义:在计算机科学中,抽象语法树(AST)是源代码语法结构的抽象表示,以树形结构展示,每个节点对应代码中的一种结构。

AST 只是一种抽象的树形结构,用于表示代码的语法。

在线可视化工具: astexplorer

estree

estree 是 ES 语法对应的标准 AST,前端开发者常用。

https://github.com/estree/estree/blob/master/es5.md

下面看一个代码 console.log('1') 对应的 AST 为:

{
  "type": "Program",
  "start": 0,
  "end": 16,
  "body": [{
    "type": "ExpressionStatement",
    "start": 0,
    "end": 16,
    "expression": {
      "type": "CallExpression",
      "callee": {
        "type": "MemberExpression",
        "object": {"type": "Identifier", "name": "console"},
        "property": {"type": "Identifier", "name": "log"},
        "computed": false
      },
      "arguments": [{"type": "Literal", "value": "1", "raw": "'1'"}]
    }
  }],
  "sourceType": "module"
}

稍微复杂的代码

const b = { a: 1 };
const { a } = b;
function add(a, b) {
  return a + b;
}

建议读者自行在 astexplorer 中查看不同节点类型。

认识 acorn

acorn 是用 JavaScript 编写的 JavaScript 解析器,类似的还有 Esprima 和 Shift。

基本操作

acorn 的使用非常简单:

import * as acorn from 'acorn';
const code = 'xxx';
const ast = acorn.parse(code, options);

options 定义如下:

interface Options {
  ecmaVersion: 3|5|6|7|8|9|10|11|12|13|2015|2016|2017|2018|2019|2020|2021|2022|'latest';
  sourceType?: 'script'|'module';
  // ... other optional flags
}

ecmaVersion 默认 es7

locations 为 true 时节点会携带位置信息

onComment 回调可获取注释内容

获取 AST 后可使用 astring 将其重新生成代码:

import * as astring from 'astring';
const code = astring.generate(ast);

实现普通函数转换为箭头函数

通过比较普通函数和箭头函数的 AST 差异,可编写代码将 FunctionDeclaration 替换为 VariableDeclaration 并创建 ArrowFunctionExpression 节点。

import * as acorn from "acorn";
import * as astring from 'astring';
import { createNode, walkNode } from "./utils.js";

const code = 'function add(a, b) { return a+b; } function dd(a) { return a + 1 }';
const ast = acorn.parse(code);

walkNode(ast, (node) => {
  if (node.type === 'FunctionDeclaration') {
    node.type = 'VariableDeclaration';
    const variableDeclaratorNode = createNode('VariableDeclarator');
    variableDeclaratorNode.id = node.id;
    delete node.id;
    const arrowFunctionExpressionNode = createNode('ArrowFunctionExpression');
    arrowFunctionExpressionNode.params = node.params;
    delete node.params;
    arrowFunctionExpressionNode.body = node.body;
    delete node.body;
    variableDeclaratorNode.init = arrowFunctionExpressionNode;
    node.declarations = [variableDeclaratorNode];
    node.kind = 'const';
  }
});

console.log('out:', astring.generate(ast));

结果如图所示:

更强大的操作可使用 recast

// 用 recast 解析并修改 AST
const ast = recast.parse(code);
const add = ast.program.body[0];
ast.program.body[0] = variableDeclaration('const', [
  variableDeclarator(add.id, functionExpression(null, add.params, add.body))
]);
const output = recast.print(ast).code;
console.log(output);

实现一个 ESLint 插件

介绍

ESLint 使用 Espree(基于 Acorn)解析 JavaScript,利用 AST 分析代码模式,完全插件化。

ESLint 配置

// .eslintrc.js
module.exports = {
  extends: ['eslint:recommended'],
  parser: '@typescript-eslint/parser',
  plugins: ['plugin1'],
  rules: {
    semi: ['error', 'always'],
    quotes: ['error', 'double'],
    'plugin1/rule1': 'error'
  }
};

Espree 对 Acorn 进行封装,将代码转为 AST。

import * as espree from "espree";
const ast = espree.parse(code);

开发一个 ESLint 插件

使用 Yeoman 初始化插件:

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

生成的插件结构示例:

no‑console 插件源码解析

/**
 * @fileoverview Rule to flag use of console object
 */
"use strict";

const astUtils = require("./utils/ast-utils");

module.exports = {
  meta: {
    type: "suggestion",
    docs: {
      description: "disallow the use of `console`",
      recommended: false,
      url: "https://eslint.org/docs/rules/no-console"
    },
    schema: [{
      type: "object",
      properties: { allow: { type: "array", items: { type: "string" }, minItems: 1, uniqueItems: true } },
      additionalProperties: false
    }],
    messages: { unexpected: "Unexpected console statement." }
  },
  create(context) {
    const options = context.options[0] || {};
    const allowed = options.allow || [];
    function isConsole(reference) { return reference.identifier && reference.identifier.name === "console"; }
    function isAllowed(node) { const name = astUtils.getStaticPropertyName(node); return name && allowed.includes(name); }
    function isMemberAccessExceptAllowed(reference) {
      const node = reference.identifier;
      const parent = node.parent;
      return parent.type === "MemberExpression" && parent.object === node && !isAllowed(parent);
    }
    function report(reference) {
      const node = reference.identifier.parent;
      context.report({ node, loc: node.loc, messageId: "unexpected" });
    }
    return { "Program:exit"() {
      const scope = context.getScope();
      const consoleVar = astUtils.getVariableByName(scope, "console");
      const shadowed = consoleVar && consoleVar.defs.length > 0;
      const references = consoleVar ? consoleVar.references : scope.through.filter(isConsole);
      if (!shadowed) {
        references.filter(isMemberAccessExceptAllowed).forEach(report);
      }
    } };
  }
};

实现一个 Stylelint 插件

Stylelint 插件使用 PostCSS AST,示例实现限制 CSS 选择器深度不超过两层:

const stylelint = require('stylelint');
const { ruleMessages, report } = stylelint.utils;
const ruleName = 'cpf-style-plugin/max-depth-2';
const messages = ruleMessages(ruleName, { expected: '不允许三层' });
module.exports = stylelint.createPlugin(ruleName, function ruleFunction() {
  return function lint(postcssRoot, postcssResult) {
    function helperDep(node, dep) {
      if (node.nodes) {
        node.nodes.forEach(newNode => {
          if (newNode.type === 'rule') {
            const selectorNum = newNode.selector.split(' ').reduce((p, c) => p + (/^[a-zA-Z.#].*/.test(c) ? 1 : 0), 0);
            if (dep + selectorNum > 2) {
              report({ message: messages.expected, node: newNode, result: postcssResult });
            }
            helperDep(newNode, dep + selectorNum);
          }
        });
      }
    }
    helperDep(postcssRoot, 0);
  };
});

stylelintrc.js 中加入插件即可生效。

实现一个 React Live Code 示例

使用 @babel/standalone 将 JSX 转为 ES5,利用 AST 找到最后一行的 React.createElement 调用,再通过 new Function 执行并渲染。

import { transform as babelTransform } from "@babel/standalone";
const tcode = babelTransform(code, { presets: ["es2015", "react"] }).code;
// AST analysis to locate React.createElement(Greet) omitted for brevity
const renderFunc = new Function("React", "render", "require", tcode);
renderFunc(React, render, require);

参考资料

estree: https://github.com/estree/estree

acorn: https://github.com/acornjs/acorn

Esprima: https://github.com/jquery/esprima

Shift: https://github.com/shapesecurity/shift-parser-js

Espree: https://github.com/eslint/espree

astring: https://www.npmjs.com/package/astring

recast: https://www.npmjs.com/package/recast

react-simple-code-editor: https://www.npmjs.com/package/react-simple-code-editor

@babel/standalone: https://babeljs.io/docs/en/babel-standalone

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.