Инструменты для сравнения входного и выходного 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) при грамотной индексации.