AST (Abstract Syntax Tree) в JavaScript представляет собой структурированное дерево, отражающее синтаксис исходного кода. ESLint строит поверх него модель обхода, в которой каждое правило подключается как набор обработчиков узлов. Именно механизм обхода AST определяет, как и когда правило «видит» конкретные конструкции кода и принимает решение о сообщении об ошибке.
ESLint использует формат ESTree. Любой файл превращается в корневой
узел Program, внутри которого находятся:
VariableDeclaration,
FunctionDeclaration)ExpressionStatement)ImportDeclaration,
ExportNamedDeclaration)Каждый узел содержит:
type — тип синтаксической конструкцииbody, expression,
left, right и т.д.)loc, range,
comments)Обход AST строится как рекурсивный спуск по этим связям, начиная с
Program.
Внутри ESLint работает универсальный traverser, основанный на глубинном обходе дерева (DFS). Его задача — последовательно посетить каждый узел дважды:
Это позволяет правилам реагировать как на момент обнаружения конструкции, так и на завершение анализа её дочерних элементов.
Упрощённая модель выглядит так:
enterexitТакой порядок гарантирует предсказуемую последовательность анализа и позволяет учитывать контекст вложенности.
ESLint не «угадывает» структуру узлов. Для каждого типа используется набор visitor keys — список свойств, содержащих дочерние узлы.
Пример:
IfStatement → test,
consequent, alternateBinaryExpression → left,
rightCallExpression → callee,
argumentsЭти ключи задаются в eslint-visitor-keys. Именно они
определяют, куда движется обход.
Если у узла нет visitor keys, он считается листовым и обход не углубляется.
Каждое правило ESLint — это функция, возвращающая объект с обработчиками узлов:
create(context) {
return {
Identifier(node) {
// логика проверки
}
};
}
Этот объект называется visitor. ESLint связывает его с обходчиком AST.
Когда traverser попадает в узел Identifier, он
проверяет:
Таким образом правило не управляет обходом напрямую — оно «подключается» к уже существующему traversal engine.
Если несколько правил слушают один и тот же тип узла, ESLint вызывает их последовательно в рамках одного посещения.
Порядок определяется внутренним registry правил и не зависит от структуры AST.
Сценарий выглядит так:
enter-обработчики для этого типаexit-обработчикиЭто важно для правил, которые зависят от контекста вложенности, например анализа областей видимости или цепочек вызовов.
Каждый обработчик получает context, через который
доступен SourceCode:
range)AST-обход сам по себе не содержит текстовой информации — он оперирует
структурой. SourceCode связывает структуру с исходным
текстом.
Пример взаимодействия:
BinaryExpressioncontext.getSourceCode().getText(node)Это разделение позволяет traversal engine оставаться независимым от текстового представления.
Обход AST реализуется через стек вызовов, соответствующий глубине вложенности.
Для кода:
function a() {
if (x) {
return y;
}
}
последовательность обхода будет:
После этого происходит «разворачивание» стека (exit-фаза), если используются exit-обработчики.
Помимо прямого перечисления типов узлов ESLint поддерживает CSS-подобные селекторы AST:
"CallExpression Identifier"
или:
"FunctionDeclaration > BlockStatement"
Этот механизм работает не как отдельный traversal, а как фильтр поверх уже существующего обхода. Во время посещения узла выполняется проверка соответствия селектору.
Это добавляет дополнительный слой:
Ключевой момент архитектуры ESLint — AST обходится один раз для всех правил.
Вместо:
используется:
Это снижает сложность с O(R × N) до O(N), где:
Каждое правило лишь «подписывается» на интересующие типы узлов.
Во время traversal ESLint параллельно строит scope tree:
Каждый узел может создавать новую область видимости.
При входе в FunctionDeclaration создаётся новый scope,
который наследует родительский. Это позволяет правилам
анализировать:
AST обход и анализ scope тесно связаны, но реализованы как отдельные проходы поверх одной структуры.
Двойной проход по узлу (enter/exit) позволяет строить сложные состояния.
Пример:
Это особенно важно для правил:
Некоторые правила могут модифицировать AST (через ESLint fixer API). Однако traversal engine не пересчитывает дерево мгновенно.
Процесс выглядит так:
Это исключает нестабильность traversal во время модификации структуры.
Разные конструкции JavaScript приводят к разной глубине и форме обхода:
IfStatement создаёт ветвление, но обход остаётся
линейным: сначала test, затем consequent, затем alternate.
CallExpression имеет переменное количество аргументов,
поэтому visitor keys включают массив arguments.
Функции всегда создают новый поддеревянный scope, что увеличивает глубину traversal.
Literal — конечный узел, traversal не продолжается.
Порядок посещения узлов строго детерминирован:
Это критично для предсказуемости правил, особенно в больших кодовых базах, где несколько правил зависят от порядка анализа.
Каждый раз, когда обработчик вызывает context.report, он
делает это в контексте текущего узла.
ESLint сохраняет:
Таким образом, обход AST — это не просто навигация по структуре, а основа для формирования статического анализа и точек диагностики.
Обход AST в ESLint можно представить как конвейер:
Эта архитектура делает возможным масштабирование правил без изменения самого механизма обхода, сохраняя единый и предсказуемый путь прохождения по синтаксическому дереву.