Внутри AST: Как инструменты понимают код
Каждый раз, когда ESLint обнаруживает проблему до того, как вы запустите код, или Prettier переформатирует неаккуратную функцию в момент сохранения, под капотом происходит что-то точное и конкретное. Эти инструменты не читают ваш исходный код как текст. Они читают его как структурированное дерево. Понимание этого дерева — абстрактного синтаксического дерева (AST) — объясняет, как работает практически каждый современный инструмент разработчика.
Ключевые выводы
- AST — это древовидное представление структуры вашего кода, часто очищенное от поверхностных деталей, таких как пробелы и группирующие скобки.
- Линтеры, форматтеры и компиляторы работают, разбирая исходный код в AST, обходя его и применяя правила или преобразования к каждому узлу.
- Паттерн посетителя (visitor pattern) является доминирующим подходом: инструменты регистрируют обработчики для конкретных типов узлов и реагируют по мере обхода дерева.
- Конкретное синтаксическое дерево (CST) сохраняет каждый токен, что делает его более подходящим для редакторов, которым требуется инкрементальный парсинг и полное представление.
- Инструменты на базе Rust, такие как Biome и Oxc, используют ту же модель «парсинг-обход-действие», но обеспечивают производительность, значительно превосходящую многие парсеры на JavaScript.
Что такое абстрактное синтаксическое дерево?
Когда инструмент разбирает ваш исходный код, он проходит через два этапа.
Сначала лексер (или токенизатор) разбивает исходный текст на токены: ключевые слова, идентификаторы, операторы, знаки пунктуации. Затем парсер берет эти токены и строит дерево, представляющее грамматическую структуру вашего кода.
Это дерево и есть AST. Оно называется «абстрактным», потому что часто отбрасывает поверхностные детали — пробелы, большую часть пунктуации и скобки, используемые исключительно для группировки, — сохраняя при этом семантически значимое.
Возьмём эту строку:
const x = 5 + 3
Парсер создаёт что-то вроде этого:
{
"type": "VariableDeclaration",
"kind": "const",
"declarations": [{
"type": "VariableDeclarator",
"id": { "type": "Identifier", "name": "x" },
"init": {
"type": "BinaryExpression",
"operator": "+",
"left": { "type": "Literal", "value": 5 },
"right": { "type": "Literal", "value": 3 }
}
}]
}
Каждый узел имеет type. Дочерние узлы вложены в родительские узлы. Вся программа становится деревом с корнем в узле Program.
Вы можете изучить это напрямую с помощью AST Explorer, который позволяет вставить любой код на JavaScript или TypeScript и в реальном времени просмотреть результирующее дерево.
Как линтеры и форматтеры используют AST
Получив AST, инструмент может сделать что-то полезное: обойти его.
Большинство инструментов используют паттерн посетителя. Вы регистрируете функцию для конкретного типа узла, и движок обхода вызывает эту функцию каждый раз, когда встречает соответствующий узел.
ESLint работает именно так. Каждое правило линтинга — это объект-посетитель. Когда ESLint обходит AST, он вызывает соответствующие обработчики правил на каждом узле. Правило, запрещающее == в пользу ===, просто слушает узлы BinaryExpression и проверяет свойство operator.
Babel использует тот же подход для преобразования кода. Он разбирает исходный код в AST, применяет преобразования на основе посетителей, которые изменяют узлы, затем печатает изменённое дерево обратно в исходный код. Так он компилирует современный синтаксис JavaScript в более старые эквиваленты.
Prettier использует другой подход. Он разбирает код в AST, отбрасывает всё исходное форматирование и перепечатывает дерево согласно своим собственным правилам компоновки. AST является источником истины — не исходный текст.
Discover how at OpenReplay.com.
AST против CST: когда структура имеет большее значение
AST опускает токены, которые синтаксически необходимы, но семантически избыточны. Конкретное синтаксическое дерево (CST) сохраняет полную синтаксическую структуру исходного кода, сохраняя токены и их позиции.
Tree-sitter создаёт конкретное синтаксическое дерево, оптимизированное для инкрементального парсинга. Поскольку оно может обновлять только ту часть дерева, которая затронута редактированием, оно хорошо подходит для подсветки синтаксиса, сворачивания кода и структурного редактирования в современных редакторах, таких как Neovim и Zed.
Переход к высокопроизводительному парсингу
Новые инструменты в экосистеме JavaScript значительно продвигают производительность парсинга вперёд. Проекты вроде Biome и Oxc реализованы на Rust и создают собственные парсеры и представления AST с нуля.
Они выполняют линтинг и форматирование со скоростью, которая часто превосходит парсеры на JavaScript, при этом поддерживая современный синтаксис, такой как атрибуты импорта и последние возможности TypeScript.
Базовая модель остаётся той же — парсинг в дерево, обход, применение анализа или преобразования — но реализация оптимизирована для масштаба.
Заключение
Пишете ли вы пользовательское правило ESLint, создаёте кодмод с помощью jscodeshift или просто пытаетесь понять, почему инструмент ведёт себя определённым образом, AST — это правильное место для поиска ответов. Это структурированное представление, на котором построен каждый серьёзный инструмент разработчика. Как только вы научитесь читать синтаксическое дерево, поведение линтеров, форматтеров и компиляторов перестанет казаться магией.
Часто задаваемые вопросы
Не для повседневного использования. Эти инструменты работают из коробки с разумными настройками по умолчанию. Но если вы хотите написать пользовательские правила линтинга, создать кодмоды или отладить неожиданное поведение инструмента, понимание того, как AST представляет ваш код, даёт вам чёткую ментальную модель того, что инструмент фактически делает на каждом шаге.
AST отбрасывает синтаксически необходимые, но семантически избыточные токены, такие как запятые, скобки и пробелы. CST сохраняет полную синтаксическую структуру исходного кода. CST предпочтительны в редакторах и IDE, где важны полное представление и инкрементальный парсинг, в то время как AST обычно используются линтерами, компиляторами и форматтерами.
Самый простой способ — AST Explorer на astexplorer.net. Вставьте любой фрагмент JavaScript или TypeScript, выберите парсер, такой как acorn, babel или typescript, и инструмент отобразит полное дерево в реальном времени. Он также поддерживает другие языки и позволяет экспериментировать с преобразованиями прямо в браузере.
Rust даёт этим инструментам прямой контроль над выделением и размещением памяти, избегает пауз сборки мусора и компилируется в высокооптимизированный нативный код. Это означает, что парсинг, обход и анализ выполняются значительно быстрее, что особенно заметно в больших кодовых базах с тысячами файлов.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.