Инструменты для сравнения входного и выходного AST

## Представление AST в SWC и базовые принципы сравнения В SWC (Speedy Web Compiler) преобразования JavaScript и TypeScript строятся вокруг работы с AST (Abstract Syntax Tree), определённого в пакете `swc_ecma_ast`. Любая трансформация — это функция, принимающая дерево синтаксических узлов и возвращающая новое дерево, которое затем либо компилируется в код, либо передаётся в следующую фазу пайплайна. Сравнение входного и выходного AST используется для: * валидации корректности трансформаций * отладки плагинов и кастомных преобразований * анализа побочных эффектов оптимизаций * обеспечения стабильности компилятора между версиями Ключевая сложность заключается в том, что AST в SWC содержит метаданные (spans, hygiene, source maps), которые делают прямое сравнение структур без предварительной нормализации практически бесполезным. --- ## Формы представления AST в экосистеме SWC ### Структурный AST (`swc_ecma_ast`) Основная модель данных — строго типизированные структуры Rust. В JavaScript через `@swc/core` доступ к ним косвенный, через сериализацию или трансформационные хуки. Типичный узел: ```ts { type: "BinaryExpression", operator: "+", left: {...}, right: {...} } ``` Однако в реальности структура включает: * `span`: позиционные данные * вложенные enum-типы * идентификаторы с hygiene-информацией * дополнительные поля трансформаций --- ### Сериализация AST Для сравнения часто используется JSON-представление: * Rust: `serde` сериализация * JS: `JSON.parse(JSON.stringify(ast))` (через `@swc/core` или плагины) Проблема: сериализация включает нестабильные поля (`span`, `marks`), которые необходимо фильтровать. --- ## Инструменты извлечения входного и выходного AST ### @swc/core как основной инструмент захвата дерева В Node.js окружении ключевым интерфейсом является пакет `@swc/core`. ```js import { parseSync, transformSync } from "@swc/core"; const input = ` const a = 1 + 2; `; const ast = parseSync(input, { syntax: "ecmascript", }); const output = transformSync(input, { jsc: { parser: { syntax: "ecmascript" }, target: "es2020", }, configFile: false, }); ``` Хотя `transformSync` возвращает код, AST можно извлекать через: * plugin API * кастомные visitor-плагины * промежуточное логирование в Rust-расширениях --- ### swc_ecma_parser (Rust уровень) На уровне Rust используется `swc_ecma_parser`: ```rust use swc_ecma_parser::{Parser, StringInput, Syntax, EsConfig}; use swc_common::SourceFile; let fm = cm.new_source_file(FileName::Custom("test.js".into()), code.into()); let mut parser = Parser::new( Syntax::Es(EsConfig::default()), StringInput::from(&*fm), None, ); let module = parser.parse_module().unwrap(); ``` Этот AST далее может быть передан в трансформации и сохранён до/после прохода. --- ## Нормализация AST перед сравнением Прямое сравнение AST почти всегда даёт ложные различия. Основная причина — метаданные. ### Удаление нестабильных полей Типичный набор нормализации: * `span.start / span.end` обнуляются * удаляются `comments` * игнорируются `source_map` ссылки * выравниваются `marks` после hygiene pass Псевдопроцедура: ```js function normalize(node) { if (!node || typeof node !== "object") return node; const clone = { ...node }; delete clone.span; delete clone.comments; for (const key in clone) { clone[key] = normalize(clone[key]); } return clone; } ``` --- ### Hygiene pass как источник различий SWC применяет hygiene (разрешение конфликтов идентификаторов). Это приводит к: * изменению внутренних идентификаторов * добавлению меток scope * модификации символов Поэтому сравнение AST до и после трансформации без учета hygiene некорректно. --- ## Основные стратегии сравнения AST ### 1. Структурный diff Прямое сравнение JSON-деревьев после нормализации: * рекурсивный обход * сравнение типов узлов * сравнение значимых полей Используется в unit-тестах трансформеров. --- ### 2. Сравнение через кодогенерацию AST преобразуется обратно в код: * SWC codegen (`@swc/core` или `swc_ecma_codegen`) * сравнение строкового представления Преимущество: * игнорирует большинство метаданных * ближе к реальному результату компиляции Недостаток: * возможны различия форматирования --- ### 3. Snapshot-тестирование Часто используется Jest: ```js import { transformSync } from "@swc/core"; test("transform", () => { const result = transformSync("const a = 1 + 2;", { jsc: { parser: { syntax: "ecmascript" } }, }); expect(result.code).toMatchSnapshot(); }); ``` Для AST snapshot применяют: * сериализацию с нормализацией * удаление span-полей * стабильную сортировку объектов --- ### 4. Дифф структур через visitor SWC предоставляет `swc_ecma_visit`, позволяющий обходить дерево: * построение списков узлов * сравнение последовательностей типов * выявление трансформаций на уровне паттернов Пример подхода: * собрать список `Identifier` до и после * сравнить множества имён * проверить изменения в `CallExpression` --- ## Специализированные инструменты сравнения ### Visitor-based capture pipeline Типичная архитектура: 1. Visitor A: фиксирует входной AST 2. Transform pipeline SWC 3. Visitor B: фиксирует выходной AST 4. Diff layer: сравнивает нормализованные деревья --- ### AST diff utilities В экосистеме SWC часто создаются кастомные diff-утилиты: * рекурсивный обход узлов * сравнение по `kind` и discriminant полям * игнорирование span и auxiliary data Пример логики сравнения: * если узлы разных типов → различие * если массивы разной длины → различие * если примитивы не совпадают → различие --- ### Сравнение через промежуточный IR Некоторые трансформации переводят AST в промежуточные формы: * flattened statements * SSA-подобные структуры (в оптимизациях) * normalized expression trees Сравнение проводится уже на уровне IR, а не исходного AST. --- ## Проблемы точного сравнения AST в SWC ### Нестабильность порядка узлов После некоторых оптимизаций: * порядок выражений может изменяться * inline-оптимизации меняют структуру дерева Это требует: * сортировки узлов перед сравнением * или перехода к set-based comparison --- ### Потери информации при codegen Codegen может: * удалять синтаксический сахар * упрощать конструкции * объединять выражения Поэтому AST ≠ generated code структура. --- ### Source maps и spans Поля `span`: * привязаны к исходному коду * меняются при любой трансформации * создают шум в diff Часто полностью исключаются из анализа. --- ## Пайплайн сравнения входного и выходного AST Типовая схема: 1. Парсинг входного кода → AST₁ 2. Нормализация AST₁ 3. Применение трансформации SWC 4. Получение AST₂ 5. Нормализация AST₂ 6. Сравнение: * structural diff * или codegen diff * или hybrid подход --- ## Пример полного цикла сравнения ```js import { parseSync, transformSync } from "@swc/core"; const code = `const x = 1 + 2;`; const inputAst = parseSync(code, { syntax: "ecmascript", }); const output = transformSync(code, { jsc: { parser: { syntax: "ecmascript" }, target: "es2020", }, }); function strip(node) { if (!node || typeof node !== "object") return node; const out = Array.isArray(node) ? [] : {}; for (const k in node) { if (k === "span" || k === "comments") continue; out[k] = strip(node[k]); } return out; } const normalizedInput = strip(inputAst); const normalizedOutput = strip(parseSync(output.code, { syntax: "ecmascript" })); const isEqual = JSON.stringify(normalizedInput) === JSON.stringify(normalizedOutput); ``` --- ## Особенности практического использования в тестировании SWC-трансформаций * стабильность AST важнее его полноты * минимизация diff достигается агрессивной нормализацией * предпочтение отдаётся codegen-сравнению при сложных трансформациях * visitor-based сбор используется для точечного анализа изменений --- ## Масштабируемые подходы в больших кодовых базах При анализе больших проектов: * AST сравнивается не целиком, а по поддеревьям * выделяются "hot nodes" (functions, imports, classes) * используется индексирование узлов по hash-сигнатурам * применяется инкрементальный diff вместо полного обхода Это позволяет уменьшить сложность сравнения с O(n²) до близкой к O(n log n) при грамотной индексации.