Абстрактное синтаксическое дерево (AST, Abstract Syntax Tree) представляет собой формальное дерево, описывающее структуру исходного кода программы в виде иерархии узлов. В контексте ESLint AST является центральной моделью данных, через которую реализуется анализ, проверка и преобразование JavaScript-кода.
AST строится после этапа лексического и синтаксического анализа исходного текста. На вход парсеру поступает строка кода, на выходе формируется структурированное дерево, в котором каждый узел описывает конкретную синтаксическую конструкцию.
Ключевые характеристики AST:
В экосистеме JavaScript де-факто стандартом является спецификация ESTree, на которую опирается парсер ESLint (обычно Espree).
Любое 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 описывает фиксированные значения:
{
"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 включает:
VariableDeclarationVariableDeclarator{
"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;
}
Структура:
idparamsbodygenerator, asyncBlockStatement группирует инструкции:
{
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" }
]
}
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" }
}
for (let i = 0; i < 10; i++) {}
Состоит из:
inittestupdatebodywhile (x > 0) {
x--;
}
AST сам по себе не хранит семантику областей видимости, однако используется как основа для её построения.
В процессе анализа формируются:
Каждый Identifier связывается с декларацией через
систему ссылок.
ESLint использует этот механизм для правил, связанных с:
AST обходится с использованием паттерна visitor.
Основные стратегии:
Пример логики обхода:
function traverse(node) {
visit(node);
for (const child of getChildren(node)) {
traverse(child);
}
}
В реальных инструментах анализ строится на основе событий:
enter Identifierexit FunctionDeclarationПарсер приводит код к единой форме:
var, let, const разделяются
по типам декларацийArrowFunctionExpressionTemplateLiteral`Hello ${name}`
представляется как:
TemplateLiteralTemplateElementExpressionAST по умолчанию может не включать комментарии в дерево, но ESLint сохраняет их отдельно:
LineCommentBlockCommentОни используются для:
Внутренняя архитектура линтера строится вокруг трёх этапов:
Каждое правило представляет собой обработчики конкретных типов узлов:
create(context) {
return {
Identifier(node) {
// логика правила
}
};
}
Такой подход позволяет декомпозировать анализ на независимые проверки, работающие поверх единой структуры AST.