Всем привет! Меня зовут Саша. Я тимлид команды разработки личного кабинета пользователя в Банки.ру, и хочу рассказать про свой опыт изучения codemod и jscodeshift для оптимизации работы с кодом. 

Для бизнеса важна скорость, с которой мы адаптируемся к изменениям на проектах и разбираемся с техдолгом. А изменений у нас хватает — у нас выходят регулярные breaking changes вроде обновлений API и синтаксиса, изменения в UI-kit и прочее. Еще у нас много крупных проектов, которые надо поддерживать в актуальном состоянии.

Сейчас у нас процесс обновления зависимостей выглядит так: идем в репозиторий либы, ищем CHANGELOG.md, смотрим что поменялось и у себя в проекте вносим правки, с помощью поиска и замены подстроки в строке, сложные регулярные выражения и пофайловое редактирование.
Если библиотека используется несколькими проектами, например ui-kit и разные фронтенд клиента, то эти действия надо повторять в каждом проекте.

Что мы могли бы сделать? 

  • Отказаться от работы со строками и громоздкими регулярными выражениями (спойлер: мы это сделаем с помощью AST).

  • Использовать один и тот же скрипт для всех типовых преобразований кода во всех проектах.

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

Что такое code modification или codemod?

Codemod — это скрипт, который позволяет автоматизировать внесение изменений в код. 

Первый codemod написали на Python в у Facebook* еще в 2007 году, а в 2008 его выпустили на волю в opensource.

Там пользователь указывал, какие изменения нужно внести в файлах с определенным расширением и в определенной директории. Codemod рекурсивно обходил указанные директории, находил подходящие файлы и применял к ним паттерн регулярного выражения для поиска совпадений. Если находил — выводил для пользователя цветной diff на аппрув. Основная его особенность была в том, что он использовал Python regex и за счет этого поддерживал все возможности регулярных выражений на Python.

Выглядело это вот так:

Сейчас эта первая версия не используется и лежит в архиве. 

Следующий этап развития codemod — тулкит jscodeshift, который тот же Facebook выпустил в 2015 году. 

Он работает с AST вместо простых регулярных выражений, предоставляет более мощные возможности трансформации кода  и позволяет создавать сложные кодмоды для JavaScript- и TypeScript-проектов. Вот его я и решил рассмотреть в качестве инструмента, который помог бы нам упростить внесение изменений, сделать этот процесс автоматическим и консистентным. 

Тут надо сделать отступление и подробнее рассказать про AST.  

Как AST меняет работу с кодом

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

За счет этого AST позволяет работать с самим кодом, а не с отдельными строками. Нам не нужны регулярные выражения и не нужно больше беспокоиться о синтаксисе — мы просто находим на дереве конкретный узел и вносим изменения в него. Это в разы упрощает и ускоряет изменение кода. 

AST уже повсеместно применяется в разных IDE. Возможно, вы раньше ее не замечали, но если вы использовали Rename Symbol или «Найти все вхождения» — значит, работали с AST, потому что именно она обеспечивает выполнение этих функций.

Название

Описание

Для чего используется

JetBrains IDE

Использует свою надстройку под названием PSI (Program Structure Interface). Это расширенная форма AST, где каждый узел несет не только информацию о синтаксисе, но и о семантике (типы, ссылки на декларации, аннотации)

Парсинг, индексация, навигация, рефакторинг, анализ кода

Microsoft Visual Studio

Roslyn предоставляет .NET-компилятор как сервис, со своим AST (Syntax Trees) и семантикой (Semantic Model).

IntelliSense и кодогенерация, анализ и анализаторные пакеты

Visual Studio Code

VS Code сам по себе легковесен и делегирует разбор кода внешним серверным процессам (Language Servers). Каждый язык имеет свой сервер (tls, pylsp, gopls, rust-analyzer), который строит AST и анализирует его для IDE-функций

Автодополнение, переход к определению и рефакторинг

Eclipse IDE

Eclipse Java Development Tools (JDT) включает собственный Java-парсер, создающий AST из исходников.

Операции Extract Method, Rename и пр. выполняются через модификацию AST и пересборку исходног�� файла

Рефакторинг

Что дает AST в codemode?

Одно из ключевых преимуществ — это контекстное понимание кода. Мы можем не просто менять имена переменных сразу по всем проектам, а делать это с учетом контекста. AST подсветит область видимости каждой переменной, и в зависимости от этого мы сможем принять решение, в какие надо вносить правки, а в какие нет.

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

При этом jscodeshift позволяет сохранить стиль кода, не смотря на то, что AST от него абстрагируется. Это обеспечивает библиотека Recast, которая использует для парсинга JS-кода Esprima, для Typescript  — @babel/parser. jscodeshift — по сути это надстройка над Recast, которая берет на себя поиск и изменение файлов. Если хотите можете работать с этой библиотекой напрямую, но jscodeshift делает жизнь проще. 

Так как работает jcodeshift?

Процесс делится на три этапа:

  1. jscodeshift разбирает код и представляет его в виде AST.

  2. Пользователь вносит модификации в дерево — применяет преобразования, такие как переименование функций или изменение параметров. Модификаций может быть несколько, они применяются по порядку.

  3. jscodeshift перезаписывает модифицированное дерево обратно в исходный код

Модификации могут выглядеть вот так: 

В root у нас лежит уже разобранное AST-дерево, в котором мы ищем Conditional Expression с определенными аргументами. Он находит все вхождения и модифицирует их, заменяя оригинальный path на path.node.consequent.

Кстати, трансформеры (или модификации) можно писать на Typescript — то есть у них есть типизация, что еще больше упрощает процесс. 

На первый взгляд это все может показаться очень сложно и непонятно: откуда мы будем брать все эти аргументы и названия? Когда я сам начал знакомиться с jscodeshift, я тоже не мог в этом разобраться, пока не нашел утилиту AST Explorer. В нее можно закинуть свой код, полазить по распаршенному AST-дереву и найти нужные переменные.

И вот так выглядит запуск из консоли: мы выбираем трансформер, расширение и какой парсер использовать: 

Пользовательские моды

Еще один важный плюс jscodeshift, кроме работы AST, это большое коммьюнити. Он существует уже давно, и под него написано много пользовательских модов. 

Вот несколько самых интересных на мой взгляд: 

  • cpojer/js-codemod — это набор самых базовых кодмодов, которые скорее всего пригодятся в работе, вроде «добавь use-strict во все JS-файлы».

  • reactjs/react-codemod — коллекция скриптов от команды React для обновления React-приложений (переход с устаревших методов жизненного цикла на UNSAFE_-варианты, миграция с React.createClass на ES6-классы, добавление PropTypes и др.).

  • rajasegar/awesome-codemods — очень большой курируемый список ресурсов и инструментов для различных языков и фреймворков, включая ссылки на популярные jscodeshift-скрипты и репозитории.

  • @compiled/codemods — Codemod’ы для автоматического перехода с styled-components и Emotion на Compiled CSS-in-JS, с комментариями TODO для ручной доработки сложных случаев.

Что есть, кроме jscodeshift?

В качестве альтернативы можно рассмотреть утилиту Ast-grep.

Она позволяет использовать все фишки AST-деревьев: структурный поиск и замена отдельных узлов, контекстное понимание кода. При этом глубоких знаний AST не потребуется благодаря интуитивному синтаксису шаблонов: они выглядят как обычный код с использованием $-переменных (например, $PATTERN).

AST-grep написана на Rust и работает очень быстро. Она поддерживает параллельную обработку, доступна через npm, pip, cargo, Homebrew и другие менеджеры пакетов. Также есть совместимость не только с JS И TS, но и другими языками, включая Python, и поддержка YAML-конфигураций. Это делает ее универсальным инструментом для мультистековых проектов и упрощает интеграцию с CI/CD.

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

Как мы планируем использовать jscodeshift

Как я это вижу: к каждому breaking changes в changelog добавлять codemod, и создать утилиту для применения codemod при установке пакета. Утилита сравнивает текущую версию библиотеки с последней выпущенной, и, если версии не совпадают, автоматически применяет все трансформации. 

Так мы максимально быстро мигрируем на новые версии, делая минимум работы вручную. Да, разработчику надо будет заморочиться со скриптами, но это того стоит, потому что в целом по компании это всем сэкономит время.

Заключение

jscodeshift — инструмент, который может сильно упростить рефакторинг крупных проектов за счет обширных возможностей по трансформации кода и богатой экосистемы пользовательских модов.  Главная сложность — во внедрении его в процессы, потому что разработчикам надо будет привыкнуть писать codemode для больших обновлений. 

Расскажите, как у вас в компаниях? Пользуетесь ли jscodeshift или похожими инструментами? И если да, то как вы их внедряли? 

* Упоминаемый ресурс Facebook принадлежат компании Meta, признанной экстремистской на территории Российской Федерации