Структура абстрактного синтаксического дерева

Абстрактное синтаксическое дерево (AST, Abstract Syntax Tree) представляет собой формальное дерево, описывающее структуру исходного кода программы в виде иерархии узлов. В контексте ESLint AST является центральной моделью данных, через которую реализуется анализ, проверка и преобразование JavaScript-кода.

Базовая модель AST

AST строится после этапа лексического и синтаксического анализа исходного текста. На вход парсеру поступает строка кода, на выходе формируется структурированное дерево, в котором каждый узел описывает конкретную синтаксическую конструкцию.

Ключевые характеристики AST:

  • иерархическая структура
  • отсутствие лишних синтаксических деталей (скобок, пробелов, комментариев — если не включены отдельно)
  • строго типизированные узлы
  • однозначное представление программы

В экосистеме JavaScript де-факто стандартом является спецификация ESTree, на которую опирается парсер ESLint (обычно Espree).


Корневой узел Program

Любое AST начинается с корневого узла Program. Он описывает весь файл как единицу анализа.

Структура:

  • type: "Program"
  • body: массив инструкций верхнего уровня
  • sourceType: "script" или "module"

Пример:

let x = 10;
function sum(a, b) {
  return a + b;
}

AST:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": { "type": "Identifier", "name": "x" },
          "init": { "type": "Literal", "value": 10 }
        }
      ],
      "kind": "let"
    },
    {
      "type": "FunctionDeclaration",
      "id": { "type": "Identifier", "name": "sum" },
      "params": [
        { "type": "Identifier", "name": "a" },
        { "type": "Identifier", "name": "b" }
      ],
      "body": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ReturnStatement",
            "argument": {
              "type": "BinaryExpression",
              "operator": "+",
              "left": { "type": "Identifier", "name": "a" },
              "right": { "type": "Identifier", "name": "b" }
            }
          }
        ]
      }
    }
  ]
}

Основные типы узлов

AST JavaScript включает множество типов узлов, каждый из которых соответствует синтаксической конструкции языка.

Идентификаторы

Узел Identifier представляет имена переменных, функций, свойств.

{
  "type": "Identifier",
  "name": "value"
}

Роль в AST:

  • связывает объявления и использования
  • участвует в анализе области видимости

Литералы

Узел Literal описывает фиксированные значения:

  • числа
  • строки
  • булевы значения
  • null
{
  "type": "Literal",
  "value": 42
}

Выражения

Выражения формируют вычисляемые конструкции.

BinaryExpression

a + b
{
  "type": "BinaryExpression",
  "operator": "+",
  "left": { "type": "Identifier", "name": "a" },
  "right": { "type": "Identifier", "name": "b" }
}

LogicalExpression

a && b
{
  "type": "LogicalExpression",
  "operator": "&&",
  "left": { "type": "Identifier", "name": "a" },
  "right": { "type": "Identifier", "name": "b" }
}

Операторы присваивания

x = 5;
{
  "type": "AssignmentExpression",
  "operator": "=",
  "left": { "type": "Identifier", "name": "x" },
  "right": { "type": "Literal", "value": 5 }
}

Объявления переменных

const a = 1, b = 2;

AST включает:

  • VariableDeclaration
  • множество VariableDeclarator
{
  "type": "VariableDeclaration",
  "kind": "const",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": { "type": "Identifier", "name": "a" },
      "init": { "type": "Literal", "value": 1 }
    },
    {
      "type": "VariableDeclarator",
      "id": { "type": "Identifier", "name": "b" },
      "init": { "type": "Literal", "value": 2 }
    }
  ]
}

Функции

Функции представляются узлами FunctionDeclaration или FunctionExpression.

function multiply(x, y) {
  return x * y;
}

Структура:

  • id
  • params
  • body
  • generator, async

Блоки и инструкции

BlockStatement группирует инструкции:

{
  let a = 1;
  let b = 2;
}
{
  "type": "BlockStatement",
  "body": [
    { "type": "VariableDeclaration" },
    { "type": "VariableDeclaration" }
  ]
}

Дерево вызовов и выражений

Вызовы функций описываются узлом CallExpression.

console.log("test");
{
  "type": "CallExpression",
  "callee": {
    "type": "MemberExpression",
    "object": { "type": "Identifier", "name": "console" },
    "property": { "type": "Identifier", "name": "log" }
  },
  "arguments": [
    { "type": "Literal", "value": "test" }
  ]
}

MemberExpression и цепочки доступа

user.profile.name;

AST:

{
  "type": "MemberExpression",
  "object": {
    "type": "MemberExpression",
    "object": { "type": "Identifier", "name": "user" },
    "property": { "type": "Identifier", "name": "profile" }
  },
  "property": { "type": "Identifier", "name": "name" }
}

Особенность:

  • вложенная структура отражает цепочку доступа
  • важна для анализа побочных эффектов и безопасности

Условные конструкции

if (x > 10) {
  return x;
} else {
  return 0;
}

Основной узел: IfStatement

{
  "type": "IfStatement",
  "test": {
    "type": "BinaryExpression",
    "operator": ">",
    "left": { "type": "Identifier", "name": "x" },
    "right": { "type": "Literal", "value": 10 }
  },
  "consequent": { "type": "BlockStatement" },
  "alternate": { "type": "BlockStatement" }
}

Циклы

ForStatement

for (let i = 0; i < 10; i++) {}

Состоит из:

  • init
  • test
  • update
  • body

WhileStatement

while (x > 0) {
  x--;
}

Область видимости и AST

AST сам по себе не хранит семантику областей видимости, однако используется как основа для её построения.

В процессе анализа формируются:

  • глобальная область
  • функциональные области
  • блочные области

Каждый Identifier связывается с декларацией через систему ссылок.

ESLint использует этот механизм для правил, связанных с:

  • неиспользуемыми переменными
  • переопределениями
  • доступом до объявления

Посещение узлов (AST traversal)

AST обходится с использованием паттерна visitor.

Основные стратегии:

  • глубинный обход (DFS)
  • обход с входом/выходом из узла (enter/exit)

Пример логики обхода:

function traverse(node) {
  visit(node);

  for (const child of getChildren(node)) {
    traverse(child);
  }
}

В реальных инструментах анализ строится на основе событий:

  • enter Identifier
  • exit FunctionDeclaration

Нормализация структуры AST

Парсер приводит код к единой форме:

  • var, let, const разделяются по типам деклараций
  • стрелочные функции нормализуются в ArrowFunctionExpression
  • шаблонные строки превращаются в TemplateLiteral
`Hello ${name}`

представляется как:

  • TemplateLiteral
  • TemplateElement
  • Expression

Комментарии и дополнительные узлы

AST по умолчанию может не включать комментарии в дерево, но ESLint сохраняет их отдельно:

  • LineComment
  • BlockComment

Они используются для:

  • отключения правил
  • аннотаций кода
  • директив анализа

Значение AST в архитектуре ESLint

Внутренняя архитектура линтера строится вокруг трёх этапов:

  1. Парсинг исходного кода в AST
  2. Обход дерева
  3. Применение правил к узлам

Каждое правило представляет собой обработчики конкретных типов узлов:

create(context) {
  return {
    Identifier(node) {
      // логика правила
    }
  };
}

Такой подход позволяет декомпозировать анализ на независимые проверки, работающие поверх единой структуры AST.