Abstract Syntax Trees (AST) for Frontend Developers

An Abstract Syntax Tree (AST) is a tree-shaped abstract representation of a program's syntax structure in computer science. Each node on the tree represents a specific structure in the source code. The term "abstract" here means that minor details of the actual syntax are omitted.

Exploring ESTree

ESTree is the standard AST specification for ECMAScript (JavaScript) syntax, which is intuitive for frontend developers. Use the online visualization tool https://astexplorer.net/ to inspect AST structures for various languages.

Let’s analyze a simple JavaScript code snippet and its ESTree representation:

console.log('Hello');
{
  "type": "Program",
  "start": 0,
  "end": 18,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 18,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 18,
        "callee": {
          "type": "MemberExpression",
          "start": 0,
          "end": 11,
          "object": {
            "type": "Identifier",
            "start": 0,
            "end": 7,
            "name": "console"
          },
          "property": {
            "type": "Identifier",
            "start": 8,
            "end": 11,
            "name": "log"
          },
          "computed": false,
          "optional": false
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 12,
            "end": 17,
            "value": "Hello",
            "raw": "'Hello'"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}

For more complex examples like destructuring assignments or funcsion declarations, try pasting them into https://astexplorer.net/ to observe the ESTree node types.

Acorn: A JavaScript Parser

Acorn is a lightweight JavaScript parser written in JavaScript, widely used in tools like webpack and ESLint. Here’s how to use Acorn to parse JavaScript code into an AST:

import * as acorn from 'acorn';

const source = 'const x = 10;';
const ast = acorn.parse(source, {
  ecmaVersion: 'latest',
  locations: true,
  sourceType: 'module'
});

To regenerate JavaScript code from an AST, use astring:

import * as astring from 'astring';

const regeneratedCode = astring.generate(ast);
console.log(regeneratedCode); // Output: const x = 10;

Converting Function Declarations to Arrow Functions

AST manipulation allows us to automate code transformations that are difficult with string matching, such as converting function declarations to arrow functions. Here’s an example using Acorn and astring with custom traversal logic:

import * as acorn from 'acorn';
import * as astring from 'astring';

function traverse(node, callback) {
  if (node && typeof node === 'object') {
    callback(node);
    for (const key in node) {
      if (typeof node[key] === 'object') {
        traverse(node[key], callback);
      }
    }
  }
}

function createNode(type) {
  const node = { type };
  return node;
}

const source = 'function sum(a, b) { return a + b; } function square(x) { return x * x; }';
const ast = acorn.parse(source);

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

const output = astring.generate(ast);
console.log(output);
// Output: const sum = (a, b) => { return a + b; } const square = (x) => { return x * x; };

Creating an ESLint Plugin

ESLint uses Espree (based on Acorn) to parse JavaScript into AST and analyze patterns. Here’s a step-by-step guide to creating an ESLint plugin.

Plugin Structure

Install the ESLint generator and create a plugin:

npm install -g yo generator-eslint
mkdir eslint-plugin-demo && cd eslint-plugin-demo
yo eslint:plugin
yo eslint:rule

Implementing the no-getnoderef Rule

This rule disallows the getNodeRef method in certain environments:

// rules/no-getnoderef.js
module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'Disallow getNodeRef method',
      recommended: false
    },
    messages: {
      unexpected: 'Unexpected getNodeRef call'
    },
    schema: []
  },

  create(context) {
    return {
      CallExpression(node) {
        if (
          node.callee.type === 'MemberExpression' &&
          node.callee.object.type === 'ThisExpression' &&
          node.callee.property.type === 'Identifier' &&
          node.callee.property.name === 'getNodeRef'
        ) {
          context.report({
            node,
            messageId: 'unexpected'
          });
        }
      }
    };
  }
};

Testing the Rule

// tests/rules/no-getnoderef.js
const { RuleTester } = require('eslint');
const rule = require('../../rules/no-getnoderef');

const ruleTester = new RuleTester({
  parserOptions: {
    ecmaVersion: 2018,
    sourceType: 'module'
  }
});

ruleTester.run('no-getnoderef', rule, {
  valid: [
    { code: 'function getNodeRef() {}; getNodeRef();' }
  ],
  invalid: [
    {
      code: 'class Component extends Base { componentDidMount() { this.getNodeRef(); } }',
      errors: [
        {
          messageId: 'unexpected',
          type: 'CallExpression'
        }
      ]
    }
  ]
});

Publishing the Plugin

npm login
npm publish

Creating a Stylelint Plugin

Stylelint uses PostCSS to parse stylesheets into AST. Here’s an example plugin that enforces a maximum of 2-level nested selectors:

// cpf-style-plugin.js
const stylelint = require('stylelint');
const { report, ruleMessages } = stylelint.utils;

const ruleName = 'cpf-style-plugin/max-depth-2';
const messages = ruleMessages(ruleName, {
  expected: 'Maximum selector depth of 2 exceeded'
});

module.exports.ruleName = ruleName;
module.exports.messages = messages;
module.exports = stylelint.createPlugin(ruleName, function() {
  return function lint(postcssRoot, postcssResult) {
    function checkDepth(node, currentDepth) {
      if (node.nodes) {
        node.nodes.forEach(child => {
          if (child.type === 'rule') {
            const selectorCount = child.selector.split(' ') 
              .filter(part => /^[a-zA-Z.#]/.test(part)).length;
            if (currentDepth + selectorCount > 2) {
              report({
                message: messages.expected,
                node: child,
                result: postcssResult
              });
            }
            checkDepth(child, currentDepth + selectorCount);
          }
        });
      }
    }
    checkDepth(postcssRoot, 0);
  };
});

Using the Styellint Plugin

// stylelint.config.js
module.exports = {
  extends: 'stylelint-config-standard',
  plugins: ['./cpf-style-plugin.js'],
  rules: {
    'cpf-style-plugin/max-depth-2': true
  }
};

React Live Code Editor

A React live code editor evaluates user-written JSX in real time. AST manipulation helps handle module imports/exports correctly. Here’s a simplified workflow:

import { transform } from '@babel/standalone';

function evaluateCode(source) {
  const transformed = transform(source, {
    presets: ['es2015', 'react']
  }).code;
  
  const wrappedCode = `
    (function(React, render) {
      ${transformed}
    })
  `;
  
  return new Function('React', 'render', wrappedCode);
}

Tags: Abstract Syntax Tree AST Frontend Development javascript ESTree

Posted on Thu, 15 Oct 2026 16:21:12 +0000 by iceman2g