Обход дерева вручную

В SWC синтаксическое дерево (AST) представляет исходный JavaScript/TypeScript код в виде иерархической структуры узлов, где каждый узел соответствует синтаксической конструкции языка: выражению, оператору, объявлению, модулю или служебной конструкции. Все трансформации и анализ выполняются через обход этого дерева.

AST в SWC построен вокруг набора строго типизированных структур, описанных в @swc/core и внутренних модулях трансформации. Ключевая особенность — отсутствие «магии»: обход дерева вручную предполагает прямую работу с узлами и их полями без автоматических высокоуровневых абстракций.


Структура узлов и базовые принципы

Каждый узел AST в SWC содержит:

  • type — строковый идентификатор вида узла
  • вложенные поля (например, body, expression, left, right)
  • дополнительные метаданные (положение в коде, комментарии)
  • дочерние узлы, которые образуют дальнейшую структуру дерева

Пример типичных узлов:

  • Program — корень модуля
  • ModuleItem — элемент верхнего уровня (импорт, экспорт, statement)
  • Stmt — оператор
  • Expr — выражение
  • Ident — идентификатор
  • CallExpr — вызов функции

Принцип ручного обхода

Ручной обход AST в SWC сводится к рекурсивному спуску по всем возможным полям узла, содержащим дочерние узлы. В отличие от visitor-абстракций, здесь логика обхода реализуется явно.

Базовый шаблон обхода:

function traverse(node) {
    if (!node) return;

    switch (node.type) {
        case "Program":
            node.body.forEach(traverse);
            break;

        case "ExpressionStatement":
            traverse(node.expression);
            break;

        case "CallExpression":
            traverse(node.callee);
            node.arguments.forEach(arg => traverse(arg.expression));
            break;

        case "Identifier":
            break;

        default:
            for (const key in node) {
                const value = node[key];
                if (Array.isArray(value)) {
                    value.forEach(traverse);
                } else if (value && typeof value === "object") {
                    traverse(value);
                }
            }
    }
}

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


Разбор ключевых узлов при обходе

Программа (Program)

Корневой узел содержит список инструкций:

Program {
    body: ModuleItem[]
}

Обход начинается именно отсюда, и каждый элемент body может быть импортом, экспортом или выражением.


Выражения

Выражения требуют особого внимания, поскольку они рекурсивны и часто вложены:

  • BinaryExpression — содержит left и right
  • CallExpression — содержит callee и arguments
  • MemberExpression — доступ к свойству объекта

Пример обработки:

function visitExpr(expr) {
    switch (expr.type) {
        case "BinaryExpression":
            traverse(expr.left);
            traverse(expr.right);
            break;

        case "MemberExpression":
            traverse(expr.object);
            traverse(expr.property);
            break;

        case "CallExpression":
            traverse(expr.callee);
            expr.arguments.forEach(a => traverse(a.expression));
            break;
    }
}

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

Узлы Identifier являются листьями дерева. Они не содержат дочерних элементов, но часто используются как точки модификации (например, переименование переменных).

case "Identifier":
    // анализ имени
    break;

Рекурсивный обход с универсальным диспетчером

Для больших AST удобнее использовать универсальный обходчик с динамическим анализом полей:

function traverse(node) {
    if (!node || typeof node !== "object") return;

    for (const key of Object.keys(node)) {
        const value = node[key];

        if (Array.isArray(value)) {
            for (const item of value) {
                traverse(item);
            }
        } else if (value && typeof value === "object") {
            traverse(value);
        }
    }
}

Этот метод не требует знания структуры узлов, но может заходить в служебные поля, поэтому иногда требует фильтрации.


Контроль типов узлов

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

Пример выборочного анализа:

function traverse(node) {
    if (!node) return;

    if (node.type === "VariableDeclarator") {
        traverse(node.id);
        traverse(node.init);
        return;
    }

    if (node.type === "FunctionDeclaration") {
        traverse(node.params);
        traverse(node.body);
        return;
    }

    for (const key in node) {
        const value = node[key];
        if (value && typeof value === "object") traverse(value);
    }
}

Обход с накоплением данных

Ручной обход часто используется не только для анализа, но и для сбора информации.

Сбор идентификаторов

const identifiers = [];

function traverse(node) {
    if (!node) return;

    if (node.type === "Identifier") {
        identifiers.push(node.value);
    }

    for (const key in node) {
        const value = node[key];
        if (Array.isArray(value)) {
            value.forEach(traverse);
        } else if (value && typeof value === "object") {
            traverse(value);
        }
    }
}

Изменение AST при обходе

Ручной обход позволяет не только читать дерево, но и модифицировать его.

Переименование переменных

function traverse(node) {
    if (!node) return;

    if (node.type === "Identifier" && node.value === "oldName") {
        node.value = "newName";
    }

    for (const key in node) {
        const value = node[key];

        if (Array.isArray(value)) {
            value.forEach(traverse);
        } else if (value && typeof value === "object") {
            traverse(value);
        }
    }
}

Особенности вложенных структур

AST SWC содержит множество уровней вложенности, особенно в:

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

Пример сложной структуры:

CallExpression
 ├── MemberExpression
 │    ├── Identifier
 │    └── Identifier
 └── Arguments[]

При ручном обходе важно не пропускать поля arguments, body, consequent, alternate.


Проблема циклических ссылок

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

const SKIP_KEYS = new Set(["parent", "comments", "loc", "range"]);

function traverse(node) {
    if (!node || typeof node !== "object") return;

    for (const key in node) {
        if (SKIP_KEYS.has(key)) continue;

        const value = node[key];

        if (Array.isArray(value)) {
            value.forEach(traverse);
        } else if (value && typeof value === "object") {
            traverse(value);
        }
    }
}

Производительность обхода

Ручной DFS-обход AST имеет линейную сложность относительно числа узлов, однако реальные затраты зависят от:

  • глубины дерева
  • количества ветвлений
  • объёма проверок типов
  • частоты модификаций узлов

Оптимизация достигается за счёт:

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

Практическое применение ручного обхода

Анализ импортов

function traverse(node) {
    if (!node) return;

    if (node.type === "ImportDeclaration") {
        console.log(node.source.value);
    }

    for (const key in node) {
        const value = node[key];
        if (value && typeof value === "object") traverse(value);
    }
}

Поиск вызовов функций

function traverse(node) {
    if (!node) return;

    if (node.type === "CallExpression") {
        if (node.callee.type === "Identifier") {
            console.log(node.callee.value);
        }
    }

    for (const key in node) {
        const value = node[key];
        if (value && typeof value === "object") traverse(value);
    }
}

Ограничения ручного обхода

Ручная реализация обхода требует:

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

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


Сравнение с visitor-подходом

Хотя SWC предоставляет visitor API через @swc/core, ручной обход:

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

Visitor-структуры абстрагируют обход, но ограничивают гибкость, тогда как ручной подход полностью раскрывает структуру AST и поведение узлов.