Раздел 2 · Программирование / CS

AST-инструменты и DSL: от codemod до своего языка

senior~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

AST-инструменты и DSL: от codemod до своего языка

В парсер-серии ты строил AST своими руками. Этот урок про обратную сторону: экосистема JS уже полна готовых AST, и уметь по ним ходить значит уметь менять тысячи файлов одной командой, писать свои правила линтера и генерировать код из схем. Вторая половина урока про DSL: как растёт предметный язык, от fluent API до внешнего языка с собственным парсером, где по этой лесенке стоит остановиться и при чём тут проектирование доменов. Перехват операций из урока про метапрограммирование здесь встретится с грамматиками из парсер-серии.

AST как рабочая поверхность

У Babel, TypeScript, ESLint и Prettier общая механика: распарсить код в дерево, поработать с деревом, иногда напечатать дерево обратно в код. Различаются они тем, что делают между парсингом и печатью: трансформацию, проверку типов, линт или форматирование. Поэтому один навык, чтение и обход AST, открывает всю четвёрку.

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

Нюанс, который сэкономит час: AST бывают разные. Большая часть экосистемы (ESLint, Babel с расширениями) говорит на диалектах стандарта ESTree, а у компилятора TypeScript собственное дерево с другими именами узлов. Один и тот же const x = 1 в ESTree это VariableDeclaration, а в TS AST VariableStatement. Поэтому в эксплорере важно выбрать тот парсер, под который пишешь.

Visitor: один паттерн на всю экосистему

В уроке про GoF мы записали Visitor в полуживые: в прикладном коде его заменил pattern matching. Вот его заповедник, где он жив и обязателен: обход дерева с диспетчеризацией по типу узла. Babel-плагин это буквально объект visitor-методов, по методу на интересующий тип узла:

module.exports = function loggerPlugin({ types: t }) {
  return {
    visitor: {
      CallExpression(path) {
        if (path.get('callee').matchesPattern('console.log')) {
          path.node.callee = t.memberExpression(
            t.identifier('logger'),
            t.identifier('debug'),
          );
        }
      },
    },
  };
};

Babel сам обходит дерево и зовёт CallExpression на каждом вызове функции. Параметр path это не узел, а обёртка над ним со ссылками на родителя и областью видимости: path.replaceWith(node) заменяет узел, path.remove() удаляет, path.scope отвечает на вопросы про переменные (кто объявил, кто использует). Этот плагин, замена console.log на logger.debug, и есть первое домашнее задание.

Инструменты по задачам

Один навык, четыре инструмента под разные задачи:

ЗадачаИнструмент
трансформация при сборкеBabel-плагин
разовая миграция кодовой базыjscodeshift (codemod)
анализ и кодген по TS-типамts-morph
запрет анти-паттернов в CIESLint-правило

jscodeshift это станок для codemod, разовых миграций: внутри recast, который печатает обратно только изменённые узлы и сохраняет форматирование остального файла. API в стиле jQuery, коллекции и find:

export default function transformer(file, api) {
  const j = api.jscodeshift;
  return j(file.source)
    .find(j.ImportSpecifier, { imported: { name: 'oldName' } })
    .forEach((path) => {
      path.node.imported.name = 'newName';
    })
    .toSource();
}

Запуск с флагом --dry показывает дифф, ничего не трогая; так проверяют codemod перед прогоном на тысяче файлов.

ts-morph это обёртка над Compiler API TypeScript для задач, где нужны не только узлы, но и типы: пройтись по интерфейсам проекта и сгенерировать на каждый type guard, найти все вызовы функции с конкретным типом аргумента, переименовать символ с учётом семантики.

ESLint-правило это визитор, который вместо трансформации репортит: create(context) возвращает объект с обработчиками узлов, узлы можно матчить esquery-селекторами (синтаксис похож на CSS), а fixer даёт автофикс. В этом репозитории есть живой пример: правило no-complex-inline-type в tools/eslint, запрещающее слишком жирные инлайновые типы. Своё правило с автофиксом, запрет new Date() без аргументов, второе домашнее задание.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.