Парсинг кода и построение AST

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

ESLint не выполняет парсинг самостоятельно. Он делегирует эту задачу подключаемому парсеру, который превращает текст в AST. В экосистеме чаще всего используется стандартный парсер espree, однако архитектура позволяет подключать альтернативы, такие как @babel/eslint-parser или @typescript-eslint/parser.


Роль парсера в архитектуре ESLint

Парсер является первым звеном цепочки анализа кода. Его задачи:

  • преобразовать строку исходного файла в структурированное дерево;
  • определить корректность синтаксиса;
  • сформировать служебные данные (позиции токенов, комментарии, границы узлов);
  • вернуть объект AST в формате, понятном ESLint.

Парсер не интерпретирует код и не выполняет его. Он работает исключительно на уровне синтаксиса.

Ключевое требование к AST в ESLint — соответствие стандарту ESTree. Это соглашение описывает структуру узлов JavaScript-кода: выражения, объявления, операторы и другие конструкции.


Этап токенизации: разбиение кода на элементы

Перед построением AST парсер выполняет лексический анализ, или токенизацию. На этом этапе исходный код разбивается на последовательность токенов — минимальных значимых единиц языка.

Пример:

const sum = a + b;

Токены:

  • const — ключевое слово
  • sum — идентификатор
  • = — оператор присваивания
  • a — идентификатор
  • + — оператор сложения
  • b — идентификатор
  • ; — разделитель выражения

Токены не содержат иерархии. Они лишь фиксируют линейную структуру текста.


Синтаксический анализ и формирование AST

После токенизации начинается синтаксический разбор. Парсер анализирует последовательность токенов и строит дерево, отражающее грамматику языка.

Рассмотрим тот же пример:

const sum = a + b;

AST будет иметь примерно следующую структуру:

  • Program

    • VariableDeclaration

      • VariableDeclarator

        • Identifier (sum)

        • BinaryEx * pression (+)

          • Identifier (a)
          • Identifier (b)

Каждый узел AST представляет конструкцию языка и содержит:

  • type — тип узла (например, VariableDeclaration);
  • start и end — позиции в исходном коде;
  • дополнительные поля, специфичные для конструкции.

Формат ESTree и стандартизация AST

ESLint использует формат ESTree как базовый контракт между парсером и линтером. Это обеспечивает совместимость различных парсеров и правил.

Основные характеристики ESTree:

  • строгая типизация узлов;
  • фиксированная структура для каждого типа конструкции;
  • единый подход к выражениям и декларациям;
  • поддержка вложенности через дерево.

Например, условный оператор:

if (x > 0) {
  y = 1;
}

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

  • IfStatement

    • test: BinaryExpression
    • consequent: BlockStatement
    • alternate: null

Расширения AST: JSX, TypeScript и нестандартный синтаксис

Стандартный ESTree не покрывает все современные расширения JavaScript. Поэтому парсеры расширяют базовую модель.

JSX

Для React-кода добавляются узлы:

  • JSXElement
  • JSXOpeningElement
  • JSXAttribute

Пример:

<Button disabled />

AST включает JSX-структуру, не существующую в базовом JavaScript-спецификации.


TypeScript

TypeScript добавляет узлы:

  • TSInterfaceDeclaration
  • TSTypeAnnotation
  • TSEnumDeclaration

Пример:

let value: number = 10;

AST фиксирует тип аннотации как отдельный подузел.


Управление комментариями и служебными данными

Помимо AST, парсер возвращает дополнительные структуры:

  • список комментариев;
  • таблицу токенов;
  • диапазоны позиций узлов;
  • флаги режима парсинга (ecmaVersion, sourceType).

Комментарии не входят в дерево, но сохраняются отдельно, так как правила ESLint могут их анализировать (например, /* eslint-disable */).


Source Code Object в ESLint

После парсинга ESLint формирует объект SourceCode, объединяющий AST и сопутствующие данные.

Он содержит:

  • корневой узел AST;
  • массив токенов;
  • список комментариев;
  • методы навигации по коду.

Эта структура используется правилами при анализе.


Обход AST: основа работы правил ESLint

После построения AST начинается его обход. ESLint использует модель visitor pattern, где каждое правило подписывается на интересующие типы узлов.

Пример логики:

  • при входе в VariableDeclaration вызывается обработчик;
  • при выходе из узла выполняется завершающая логика;
  • вложенные узлы обрабатываются рекурсивно.

Таким образом, AST становится основой для декларативного анализа.


Пример полного процесса преобразования

Исходный код:

function add(a, b) {
  return a + b;
}

Этапы обработки:

  1. Токенизация:

    • function, add, (, a, ,, b, ), {, return, a, +, b, }
  2. Построение AST:

    • FunctionDeclaration

      • Identifier (add)

      • params: [a, b]

      • body: BlockStatement

        • ReturnStatement

          • BinaryExpression
  3. Обогащение SourceCode:

    • добавление токенов;
    • привязка комментариев;
    • индексация диапазонов.

Значение AST для линтинга

AST является центральным элементом всей системы ESLint. Без него невозможны:

  • анализ структуры кода;
  • применение правил;
  • выявление ошибок и антипаттернов;
  • модификация кода через фиксы.

Каждое правило ESLint оперирует не текстом, а структурированными узлами, что делает анализ точным и предсказуемым.


Ограничения парсинга

Несмотря на мощность, парсинг имеет ограничения:

  • некорректный синтаксис может прервать построение AST;
  • разные парсеры могут формировать несовместимые расширения узлов;
  • нестандартные конструкции требуют дополнительных плагинов;
  • большие файлы увеличивают стоимость анализа.

Тем не менее архитектура ESLint допускает гибкую замену парсера без изменения логики правил.


Роль AST в экосистеме инструментов анализа кода

AST, используемый ESLint, совпадает по концепции с деревьями в других инструментах:

  • Babel использует AST для трансформации кода;
  • TypeScript использует собственное дерево для компиляции;
  • Prettier применяет AST для форматирования.

Общая модель позволяет различным инструментам взаимодействовать через единое представление структуры кода.