Мощь AST в действии, или как переписать код 10 летней давности на ES6-модули и ничего не сломать

от автора

Всем привет! Меня зовут Кирилл и я работаю фронтенд-разработчиком. Я расскажу о том, как мы перевели несколько тысяч файлов, написанных на JavaScript, с легаси кода, который использовал goog.module, на новые ES6-модули с помощью построения и преобразования абстрактного синтаксического дерева.

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

Причины, почему мы решили переводить нашу кодовую базу

В нашем проекте мы используем Google Closure Library. Google Closure Library — это JavaScript-библиотека, предоставляющая инструменты для работы с модулями. Подробнее об этом я расскажу ниже, но также можно прочитать документацию здесь.

Минусы goog.module:

  1. Длинные пространства имён

  2. C goog-модулями плохо работают подсказки IDE

  3. Появление скрытых зависимостей

  4. Актуальность технологий

В итоге у нас появилась потребность перевода кодовой базы на ES6, но нас немного пугала необходимость внести изменения в большую кодовую базу (900 000 строк кода) и ничего при этом не сломать.

Выбор инструмента

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

Ручное изменение файлов проекта

Первое о чем мы задумались — это менять проект постепенно и вручную. Но вручную переводить код долго, неинтересно и можно ошибиться. Поэтому мы решили автоматизировать перевод.

Регулярные выражения

Этот вариант может оказаться очень даже привлекательным, и сначала нам тоже так казалось. Модули, объявленные с помощью goog.module, перевести с помощью регулярных выражений легко, так как нам нужно только удалить объявление модуля и поменять формат экспорта. Пример goog.module:

goog.module('my.module.Foo') // Достаточно удалить goog.module const googArray = goog.require('goog.array') class Foo {} exports = Foo // И поменять exports = Foo на export {Foo}

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

goog.provide('old.module.Foo') // Нужно удалить goog.provide goog.require('goog.array') // Поменять goog.require('goog.array') на const array = goog.require('goog.array')  goog.scope(() => { // Удалить goog.scope   const array = goog.array // Удалить синоним   // Избавиться от namespace'а   old.module.Foo = goog.defineClass(null, { // Изменить синтаксис классов constructor: function Foo() {       const numbers = [1, 2, 3, 4, 5];       const evenNumbers = array.filter(numbers, num => num % 2 === 0);       console.log('Четные числа:', evenNumbers); }   }) })  // Добавить экспорт export {Foo}
goog.provide('myapp.utils') // Нужно удалить goog.provide  // Избавиться от namespace'а function sum() {} myapp.utils.sum = function (a, b) { return a + b }  // Избавиться от namespace'а function mul() {} myapp.utils.mul = function (a, b) { return a * b }  // Добавить экспорт export {sum, mul}

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

Рефакторинг через преобразование структуры программы

Мы воспользовались утилитой jscodeshift. Это инструмент командной строки для автоматизированной модификации JavaScript-кода с помощью шаблонов.

jscodeshift строит абстрактное синтаксическое дерево (AST) из исходного кода, которое затем можно преобразовывать при помощи кодмодов (codemod), скриптов, которые манипулируют абстрактными синтаксическими деревьями.

Плюсы jscodeshift:

  • Декларативный стиль написания кодмодов (удобство чтения).

  • Многопоточная обработка файлов.

  • Возможность запускать кодмоды в режиме “dry run”, то есть без перезаписи обрабатываемых файлов. Это помогает во время написания кодмодов.

Минусы jscodeshift:

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

Далее расскажем подробнее про абстрактное синтаксическое дерево и jscodeshift.

Абстрактное синтаксическое дерево

Абстрактное синтаксическое дерево (Abstract Syntax Tree, AST) представляет собой структуру данных, которая описывает синтаксическую структуру программы или её фрагмента.

Вот несколько ключевых понятий AST в контексте JavaScript:

  1. Узлы: Узлы AST представляют конструкции языка JavaScript, такие как вызовы функций, объявления переменных, операторы присваивания и т.д.

  2. Типы узлов: Каждый узел AST имеет свой тип, который указывает на конкретный элемент языка JavaScript. Примеры типов узлов: «FunctionDeclaration», «VariableDeclaration», «BinaryExpression» и т.д.

  3. Листья: Листья AST представляют элементарные конструкции. Например, листьями могут быть идентификаторы переменных, строковые литералы, числовые литералы и т. д.

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

Принцип работы jscodeshift

jscodeshift работает в несколько этапов:

  1. Строит AST из исходного кода

  2. Преобразует AST с помощью кодмода

  3. Превращает AST обратно в код

Untitled

Теперь разберём представление AST в JavaScript и написание кодмода на таком примере:

goog.provide('math.add')

Визуально AST для этой программы выглядит так:

Untitled

Это представление применяется при работе с деревом из кодмод-скриптов:

{   type: 'CallExpression',   callee: {     type: 'MemberExpression',     object: {       type: 'Identifier',       name: 'goog'     },     property: {       type: 'Identifier',       name: 'provide'     }   } }

Пример кодмода, удаляющего все вызовы goog.provide:

// src/removeGoogProvideTransform.js // Пример кодмода, удаляющего все вызовы goog.provide export default removeGoogProvide(file, api) { const jscs = api.jscodeshift const root = jscs(file.source) // Получаем AST-дерево return root // Ищем CallExpression (вызовы функций) // по шаблону {object: goog, property: provide} .find(jscs.CallExpression, { callee: {     object: { name: 'goog' }, property: { name: 'provide'     } } }) // Удаляем все найденные элементы .remove() // Преврашаем AST в файл .toSource() }

Этот вызов утилиты jscodeshift удаляет из файла math/Add.js все вызовы функции goog.provide, используя написанный выше кодмод:

jscodeshift -t src/removeGoogProvideTransform.js math/Add.js # флаг -t указывает путь к кодмоду

Процесс перевода

Мы также применили jscodeshift, чтобы автоматически модернизировать объявления классов, так как в нашем коде оставалось много мест, где классы объявлялись с помощью функции goog.defineClass:

var Foo = goog.defineClass(Bar, {   constructor: function() {...}   doSomething: function() {...} })  

Кодмод преобразовывал объявление классов функцией goog.defineClass в объявление с синтаксис ES6:

class Foo extends Bar {     constructor() {}     doSomething() {} } 

После того, как мы написали скрипт, мы запустили его на трёх небольших проектах (всего около 500 файлов). Перевод занял 1 час, который ушёл на запуск скрипта и ручную правку некоторых сложных моментов.

Затем мы провели обновление кода самого большого из наших проектов. Эта работа прошла в несколько этапов:

  1. Автоматическое обновление исходников с помощью кодмодов.

  2. Исправление ошибок и предупреждений сборки, которые проявились из-за того, что мы автоматизировали только часто встречающиеся паттерны. Недочёты, остающиеся в коде в нескольких местах, быстрее оказалось исправить вручную, чем писать и отлаживать кодмод.

Тестирование основных пользовательских кейсов проекта заняло около недели. После чего мы зарелизились и, кажется, ничего не сломали?.

Итоги

Всего на данный момент мы изменили 3 908 файлов в 4 проектах. Мы планируем продолжать рефакторинг, чтобы в конечном итоге полностью избавиться от старых модулей в коде.

Использование jscodeshift значительно помогло нам в этом преобразовании, поэтому мы советуем вам тоже попробовать этот инструмент, чтобы ускорить процесс рефакторинга.

Советы

Если у вас есть похожая задача по обновлению большой кодовой базы, хотим поделиться несколькими рекомендациями:

  1. Начинайте с маленьких проектов. Лучше переведите сначала маленькие проекты, у которых нет внешних зависимостей или их мало. Обкатайте решение на них. После этого будет проще переводить большие проекты.

  2. Умейте остановиться. Рекомендуем автоматически трансформировать только часто встречающиеся паттерны. Недочёты, остающиеся в коде в нескольких местах, оказалось исправить вручную быстрее, чем писать и отлаживать кодмод.

Полезные ссылки

Мы собрали список ссылок с полезными материалами, которые могут помочь вам в переводе goog-модулей на ES6-модули

  1. https://github.com/google/closure-compiler/wiki/Migrating-from-goog.modules-to-ES6-modules — Статья от Google по миграции с goog-модулей на ES6-синтаксис

  2. https://github.com/facebook/jscodeshift — jscodeshift

  3. https://www.youtube.com/watch?v=-YZt2DW75h8 — доклад от Александра Мышова по jscodeshift, который поможет понять принцип работы с jscodeshift

  4. https://github.com/schmidtk/opensphere-jscodeshift/ — мы вдохновлялся фрагментами кода из этого репозитория, где разработчики решали проблему подобную нашей

  5. https://astexplorer.net — построение AST дерева кода, полезно при использовании jscodeshift

  6. https://doc.esdoc.org/github.com/mason-lang/esast/ — Описание узлов синтаксического дерева для JavaScript


ссылка на оригинал статьи https://habr.com/ru/articles/822975/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *