Если вы разрабатываете библиотеку, например, такую как lodash или React, ваша утилита может стать популярной и использоваться сотнями тысяч разработчиков ежедневно. Со временем могут появиться шаблоны использования, выходящие за рамки изначального замысла. В таких случаях может возникнуть необходимость расширить API, добавив параметры или изменив сигнатуры функций для обработки крайних случаев. Главная сложность — внедрить эти изменения, не нарушая работу пользователей.
Именно здесь на сцену выходят codemods — мощный инструмент для автоматизации масштабных преобразований кода. Они позволяют разработчикам вносить критические изменения API, рефакторить устаревшие кодовые базы и поддерживать чистоту кода с минимальными ручными правками.
В этой статье мы разберём, что такое codemods и какие инструменты можно использовать для их создания, например, jscodeshift, hypermod.io и codemod.com. Мы рассмотрим реальные примеры — от удаления feature toggles до рефакторинга иерархий компонентов. Вы также узнаете, как разбивать сложные преобразования на более мелкие, тестируемые части — этот подход известен как композиция codemods и помогает обеспечить гибкость и поддерживаемость кода.
К концу статьи вы поймёте, как codemods могут стать важной частью вашего инструментария для работы с крупными кодовыми базами, помогая поддерживать их в чистоте и управляемости, даже при решении самых сложных задач по рефакторингу.
Содержание
Критические изменения в API
Вернёмся к сценарию разработчика библиотеки. После первого релиза появляются новые способы использования, что требует расширения API — например, добавления параметра или изменения сигнатуры функции для удобства работы.
Для простых изменений может подойти обычный поиск и замена в IDE. В более сложных случаях можно воспользоваться инструментами вроде sed или awk. Однако, если ваша библиотека широко используется, такие изменения становятся сложнее в управлении. Вы не можете точно предсказать, насколько сильно модификация повлияет на пользователей; а последнее, чего вам хочется, — это сломать существующую функциональность, которая не требует обновления.
Один из распространённых подходов — объявить критическое изменение, выпустить новую версию и позволить пользователям самостоятельно адаптироваться. Однако такой процесс, хотя и привычен, часто не масштабируется, особенно при кардинальных изменениях. Вспомните переход React от классовых компонентов к функциональным с хуками — этот сдвиг потребовал несколько лет, чтобы крупные кодовые базы смогли полностью адаптироваться. А к моменту, когда команды заканчивали миграцию, уже появлялись новые критические изменения.
Для разработчиков библиотек это создаёт серьёзную нагрузку: поддержка множества устаревших версий ради пользователей, которые ещё не обновились, требует значительных затрат времени и ресурсов. Также частые изменения подрывают доверие пользователей: они могут начать избегать обновлений или искать более стабильные альтернативы, тем самым усугубляя проблему.
Что если пользователи могли бы адаптироваться к изменениям автоматически? Что, если вместе с обновлением вы могли бы предложить инструмент, который сам рефакторит их код — переименовывает функции, меняет порядок параметров, удаляет устаревший код, избавляя их от ручных правок?
Здесь и вступают в игру codemods. Несколько популярных библиотек, включая React и Next.js, уже используют их, чтобы облегчить переход между версиями. Например, React предоставляет codemods для миграции от устаревших API-паттернов, таких как старый Context API, к новым решениям.
Но что же такое codemod в данном контексте?
Что такое codemod?
Codemod (code modification) — это автоматизированный скрипт, который изменяет код, чтобы привести его в соответствие с новыми API, синтаксисом или стандартами кодирования. Codemods используют манипуляцию абстрактным синтаксическим деревом (AST), чтобы применять единообразные, масштабные изменения в кодовой базе.
Изначально разработанные в Facebook, codemods помогали инженерам справляться с задачами рефакторинга в крупных проектах, таких как React. По мере роста Facebook сопровождение кодовой базы и обновление API становилось всё более сложной задачей, что привело к созданию codemods.
Ручное обновление тысяч файлов в разных репозиториях оказалось неэффективным и подверженным ошибкам, поэтому были разработаны автоматизированные скрипты, которые выполняли преобразование кода.
Процесс работы codemods обычно включает три основных этапа:
Разбор кода и представление его в виде AST — древовидной структуры, где каждая часть кода становится отдельным узлом.
Модификация дерева — применение преобразований, таких как переименование функций или изменение параметров.
Перезапись модифицированного дерева обратно в исходный код.
Этот подход гарантирует, что изменения применяются последовательно ко всей кодовой базе, сводя к минимуму вероятность человеческих ошибок. Codemods также могут справляться со сложными сценариями рефакторинга, такими как изменение глубоко вложенных структур или удаление вызовов устаревших API.
Если визуализировать этот процесс, он будет выглядеть следующим образом:
Идея программы, которая может «понимать» ваш код и автоматически выполнять преобразования, не нова. Именно так работает ваша IDE при запуске этапов рефакторинга, таких как Extract Function (извлечение функции), Rename Variable (переименование переменной) или Inline Function (встраивание функции). По сути, IDE разбирает исходный код в AST, применяет предопределённые преобразования к дереву и сохраняет результат обратно в файлы.
В современных IDE происходит множество процессов, обеспечивающих корректное и эффективное применение изменений — например, определение области действия рефакторинга и разрешение конфликтов, таких как конфликты имён переменных. Некоторые рефакторинги даже требуют ввода параметров перед выполнением — например, в случае Change Function Declaration (изменение объявления функции), где можно задать порядок аргументов или значения по умолчанию перед применением изменений.
Использование jscodeshift в кодовых базах на JavaScript
Рассмотрим конкретный пример использования codemod в JavaScript-проекте. В сообществе JavaScript существует множество инструментов, делающих эту задачу выполнимой. Среди них — парсеры, преобразующие исходный код в AST, а также транспиляторы, способные трансформировать AST в другие форматы (именно так работает TypeScript). Кроме того, существуют инструменты, позволяющие автоматически применять codemods ко всей кодовой базе.
Одним из самых популярных инструментов для написания codemods является jscodeshift — библиотека, поддерживаемая Facebook. Она упрощает создание codemods, предоставляя мощный API для работы с AST. С помощью jscodeshift разработчики могут находить определённые шаблоны в коде и применять преобразования в масштабе всего проекта.
С jscodeshift можно, например, автоматически обнаруживать устаревшие вызовы API и заменять их на актуальные версии по всему проекту.
Давайте разберём типичный процесс создания codemod вручную.
Очистка устаревшего feature toggle
Начнём с простого, но полезного примера, который демонстрирует возможности codemods. Представьте, что в вашей кодовой базе используется feature toggle для управления выпуском незавершённых или экспериментальных функций. После того как фича выходит в продакшен и работает как ожидается, следующим шагом становится удаление этого feature toggle и связанной с ним логики.
Например, рассмотрим следующий код:
const data = featureToggle('feature-new-product-list') ? { name: 'Product' } : undefined;
Когда фича полностью выпущена и больше не нуждается в toggle, код можно упростить до вида:
const data = { name: 'Product' };
Задача заключается в том, чтобы найти все вхождения featureToggle
в кодовой базе, проверить, относится ли toggle к feature-new-product-list
, и удалить условную логику вокруг него. При этом другие feature toggles (например, feature-search-result-refinement
, который может всё ещё находиться в разработке) должны остаться без изменений. Codemod должен анализировать структуру кода, чтобы применять изменения выборочно.
Разбор AST
Прежде чем приступить к написанию codemod, разберёмся, как этот фрагмент кода представлен в AST, что поможет понять, с какими узлами (nodes) вам предстоит работать при внесении изменений. В этом поможет инструмент AST Explorer.
На изображении ниже показано синтаксическое дерево в терминах ECMAScript. Оно содержит узлы, такие как Identifier
(для переменных), StringLiteral
(для имени feature toggle) и более сложные структуры, например, CallExpression
и ConditionalExpression
.
В этой AST-структуре переменной data присваивается значение с использованием ConditionalExpression
. В тестовой части выражения вызывается featureToggle('feature-new-product-list')
. Если тест возвращает true
, то в consequent-ветке data присваивается { name: 'Product' }
. Если false, в alternate-ветке присваивается undefined
.
Когда задача имеет чётко определённые входные и выходные данные, я предпочитаю сначала писать тесты, а затем реализовывать codemod. Начинаю с негативного сценария, чтобы убедиться, что случайные изменения в коде, которые не должны затрагиваться, действительно не применяются. Затем добавляю тест для реального сценария, в котором выполняется преобразование.
Сначала я реализую простейший сценарий, затем добавляю вариации (например, проверку вызова featureToggle
внутри if-условия), реализую этот случай и убеждаюсь, что все тесты проходят.
Этот подход хорошо согласуется с разработкой через тестирование (TDD), даже если вы не практикуете её регулярно. Чёткое понимание входных и выходных данных трансформации перед написанием кода повышает надёжность и эффективность работы, особенно при доработке codemods.
С помощью jscodeshift можно писать тесты, проверяющие, как ведёт себя codemod.
const transform = require("../remove-feature-new-product-list");
defineInlineTest(
transform,
{},
`
const data = featureToggle('feature-new-product-list') ? { name: 'Product' } : undefined;
`,
`
const data = { name: 'Product' };
`,
"delete the toggle feature-new-product-list in conditional operator"
);
Функция defineInlineTest
из jscodeshift позволяет задать входные данные, ожидаемый результат и строку с описанием теста. Запуск теста обычной командой jest
приведёт к его провалу, так как codemod ещё не написан.
Негативный тест гарантирует, что код остаётся неизменным для других feature toggles.
defineInlineTest(
transform,
{},
`
const data = featureToggle('feature-search-result-refinement') ? { name: 'Product' } : undefined;
`,
`
const data = featureToggle('feature-search-result-refinement') ? { name: 'Product' } : undefined;
`,
"do not change other feature toggles"
);
Написание codemod
Начнём с определения простой функции трансформации. Создайте файл transform.js со следующей структурой:
module.exports = function(fileInfo, api, options) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// изменяйте узлы дерева здесь
return root.toSource();
};
Эта функция загружает файл в AST-дерево и использует API jscodeshift для поиска, изменения и обновления узлов. В конце она преобразует AST обратно в исходный код с помощью .toSource()
.
Теперь можно приступить к реализации шагов трансформации:
Найти все вхождения
featureToggle
.Проверить, что переданный аргумент —
'feature-new-product-list'
.Заменить всю условную конструкцию на consequent, фактически удалив toggle.
Вот как это можно реализовать с помощью jscodeshift:
module.exports = function (fileInfo, api, options) {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// Найти ConditionalExpression, где тестом является featureToggle('feature-new-product-list')
root
.find(j.ConditionalExpression, {
test: {
callee: { name: "featureToggle" },
arguments: [{ value: "feature-new-product-list" }],
},
})
.forEach((path) => {
// Заменить ConditionalExpression на 'consequent'
j(path).replaceWith(path.node.consequent);
});
return root.toSource();
};
Codemod, описанный выше:
Находит узлы
ConditionalExpression
, в которых в тестовой части вызываетсяfeatureToggle('feature-new-product-list')
.Заменяет всю условную конструкцию на consequent (например,
{ name: 'Product' }
), удаляя логику toggle и оставляя упрощённый код.
Этот пример показывает, насколько просто создать полезную трансформацию и применить её к большой кодовой базе, значительно сократив объём ручной работы.
Чтобы сделать codemod более устойчивым к реальным сценариям, потребуется добавить больше тест-кейсов для обработки вариаций, таких как if-else
конструкции, логические выражения (например, !featureToggle('feature-new-product-list')
) и другие случаи.
Когда codemod будет готов, можно протестировать на целевой кодовой базе. jscodeshift предоставляет инструмент командной строки для его применения и анализа результатов.
$ jscodeshift -t transform-name src/
После проверки результатов убедитесь, что все функциональные тесты проходят и ничего не ломается — даже если изменения являются критическими. Если всё в порядке, можно зафиксировать изменения и создать пул-реквест в рамках стандартного рабочего процесса.
Codemods улучшают качество и поддерживаемость кода
Codemods полезны не только для адаптации к изменениям API — они также повышают качество и поддерживаемость кода. По мере развития кодовой базы в ней накапливается технический долг: устаревшие feature toggles и методы, жёстко связанные компоненты. Ручной рефакторинг таких участков занимает много времени и приводит к ошибкам.
Автоматизируя задачи по рефакторингу, codemods помогают поддерживать кодовую базу в чистоте, удаляя устаревшие шаблоны. Регулярное их использование позволяет внедрять новые стандарты кодирования, удалять неиспользуемый код и модернизировать кодовую базу без необходимости вручную менять каждый файл.
Рефакторинг компонента Avatar
Теперь рассмотрим более сложный пример. Допустим, в вашем дизайн-системе есть компонент Avatar, который жёстко связан с Tooltip. В текущей реализации, если пользователь передаёт name в Avatar, компонент автоматически оборачивает аватар в tooltip.
Вот текущая реализация Avatar:
import { Tooltip } from "@design-system/tooltip";
const Avatar = ({ name, image }: AvatarProps) => {
if (name) {
return (
<Tooltip content={name}>
<CircleImage image={image} />
</Tooltip>
);
}
return <CircleImage image={image} />;
};
Цель — отвязать Tooltip от компонента Avatar, предоставив разработчикам больше гибкости. Они должны сами решать, нужно ли оборачивать Avatar в Tooltip. В отрефакторенной версии Avatar будет просто рендерить изображение, а при необходимости пользователи смогут вручную добавлять Tooltip.
Вот отрефакторенная версия Avatar:
const Avatar = ({ image }: AvatarProps) => {
return <CircleImage image={image} />;
};
Теперь пользователи могут вручную обернуть Avatar в Tooltip по мере необходимости.
import { Tooltip } from "@design-system/tooltip";
import { Avatar } from "@design-system/avatar";
const UserProfile = () => {
return (
<Tooltip content="Juntao Qiu">
<Avatar image="/juntao.qiu.avatar.png" />
</Tooltip>
);
};
Проблема в том, что в кодовой базе могут быть сотни вызовов Avatar. Рефакторинг каждого экземпляра вручную был бы крайне неэффективным, поэтому этот процесс можно автоматизировать с помощью codemod.
Используя AST Explorer, можно проанализировать компонент и определить, какие узлы соответствуют использованию Avatar, которое нужно изменить. Компонент Avatar с пропсами (props) name и image в AST выглядит так:
Написание codemod
Разобьём процесс преобразования на более мелкие задачи:
Найти все вызовы Avatar в компонентном дереве.
Проверить, есть ли у него проп name.
Если
name
отсутствует — ничего не делать.Если name есть:
Создать Tooltip.
Передать name в Tooltip.
Удалить name из Avatar.
Добавить Avatar в качестве дочернего элемента Tooltip.
Заменить оригинальный Avatar новой структурой.
Для начала найдём все вхождения Avatar (некоторые тесты опущены, но сначала стоит написать сравнительные тесты).
defineInlineTest(
{ default: transform, parser: "tsx" },
{},
`
<Avatar name="Juntao Qiu" image="/juntao.qiu.avatar.png" />
`,
`
<Tooltip content="Juntao Qiu">
<Avatar image="/juntao.qiu.avatar.png" />
</Tooltip>
`,
"wrap avatar with tooltip when name is provided"
);
Аналогично примеру с featureToggle
, можно использовать root.find с нужными критериями поиска для нахождения всех узлов Avatar:
root
.find(j.JSXElement, {
openingElement: { name: { name: "Avatar" } },
})
.forEach((path) => {
// теперь мы можем обработать каждую инстанцию Avatar
});
Далее мы проверяем, присутствует ли пропс name:
root
.find(j.JSXElement, {
openingElement: { name: { name: "Avatar" } },
})
.forEach((path) => {
const avatarNode = path.node;
const nameAttr = avatarNode.openingElement.attributes.find(
(attr) => attr.name.name === "name"
);
if (nameAttr) {
const tooltipElement = createTooltipElement(
nameAttr.value.value,
avatarNode
);
j(path).replaceWith(tooltipElement);
}
});
В функции createTooltipElement
используется API jscodeshift для создания нового узла JSX, в котором проп name применяется к Tooltip, а компонент Avatar становится его дочерним элементом. В финальном шаге replaceWith
заменяет старый узел новой структурой.
Вот предварительный результат в Hypermod: слева написан codemod, сверху справа — оригинальный код, а снизу справа — преобразованный результат.
Этот codemod ищет все вхождения Avatar. Если найден проп name, он удаляет name из Avatar, оборачивает Avatar в Tooltip и передаёт name в Tooltip.
К этому моменту должно быть очевидно, что codemods невероятно полезны, а процесс их использования логичен и удобен, особенно при масштабных изменениях, где ручное обновление стало бы огромной нагрузкой. Однако это ещё не вся картина. В следующем разделе мы рассмотрим некоторые сложности и способы их решения.
Исправление типичных ошибок при использовании codemods
Как опытный разработчик, вы знаете, что «идеальный сценарий» охватывает лишь небольшую часть реальной картины. При написании скрипта преобразования кода нужно учитывать множество различных ситуаций.
Разработчики пишут код в разных стилях. Например, кто-то может импортировать компонент Avatar, но присвоить ему другое имя, если в проекте уже есть другой Avatar из другого пакета:
import { Avatar as AKAvatar } from "@design-system/avatar";
const UserInfo = () => (
<AKAvatar name="Juntao Qiu" image="/juntao.qiu.avatar.png" />
);
В таком случае простой поиск текста Avatar не сработает. Потребуется обнаружить псевдоним и применить преобразование с учётом правильного имени.
Другой пример связан с импортами Tooltip. Если файл уже импортирует Tooltip, но под другим именем, codemod должен распознать этот псевдоним и применить изменения соответствующим образом. Нельзя считать, что компонент с именем Tooltip всегда является нужным для изменения.
В случае с feature toggle возможны и другие варианты: разработчик может использовать if(featureToggle('feature-new-product-list'))
или присваивать результат работы toggle-переменной перед её использованием:
const shouldEnableNewFeature = featureToggle('feature-new-product-list');
if (shouldEnableNewFeature) {
//...
}
Могут встречаться и более сложные конструкции, например, использование toggle в логических выражениях или с отрицанием, что усложняет обработку кода:
const shouldEnableNewFeature = featureToggle('feature-new-product-list');
if (!shouldEnableNewFeature && someOtherLogic) {
//...
}
Такие вариации затрудняют предсказание всех возможных пограничных случаев и увеличивают риск внесения неожиданных изменений. Полагаться только на заранее известные сценарии недостаточно — необходим тщательный тестинг, чтобы избежать нежелательных модификаций.
Использование графов исходного кода и подхода Test-Driven Codemods
Чтобы справиться с этой сложностью, codemods следует применять вместе с другими техниками. Несколько лет назад я участвовал в проекте переписывания компонентов дизайн-системы в Atlassian. Мы решили эту проблему, сначала исследовав граф исходного кода, который содержал информацию о большинстве внутренних использований компонентов. Это позволило нам понять, как именно применяются компоненты, импортируются ли они под разными именами, а также какие публичные пропсы используются чаще всего.
После этапа анализа мы сначала написали тест-кейсы, чтобы охватить максимальное количество сценариев, а затем разработали сам codemod.
В тех случаях, когда автоматическое обновление было недостаточно надёжным, мы добавляли комментарии или TODO-метки в нужных местах кода. Это позволяло разработчикам, запускающим скрипт, вручную обработать сложные случаи. Обычно таких случаев было немного, поэтому этот подход оставался эффективным для обновления кодовой базы.
Использование инструментов стандартизации кода
Как видно, в сложных кодовых базах, особенно тех, которые включают внешние зависимости, возникает множество пограничных случаев. Поэтому codemods требуют внимательной проверки и ревью результатов.
Однако, если в вашем проекте уже используются инструменты стандартизации кода (например, линтер, который соблюдает определённый стиль кодирования), их можно использовать для сокращения числа пограничных случаев. Линтеры обеспечивают единообразие кода, что упрощает преобразования и уменьшает риск ошибок при рефакторинге.
Линтер можно настроить так, чтобы он запрещал определённые конструкции (например, вложенные тернарные операторы) и рекомендовал использовать именованные экспорты вместо экспортов по умолчанию.
Кроме того, разбивка сложных преобразований на более мелкие и управляемые этапы позволяет точнее обрабатывать отдельные изменения. Как мы увидим дальше, комбинирование небольших codemods упрощает работу с масштабными изменениями.
Выделение переиспользуемых утилит
Даже при хорошо продуманной стратегии могут оставаться повторяющиеся задачи — например, добавление импорта, если он отсутствует, вставка комментариев в определённых местах или переименование пропсов. Такие операции часто встречаются в различных преобразованиях.
Со временем мы разработали набор вспомогательных функций, которые упрощают написание codemods. Эти утилиты позволяют автоматизировать общие задачи более эффективно. Один из примеров — объединение небольших утилит в более сложные трансформеры, о чём мы поговорим в следующем разделе.
Композиция codemods
Вернёмся к примеру удаления feature toggle. В приведённом коде есть toggle feature-convert-new, который необходимо удалить:
import { featureToggle } from "./utils/featureToggle";
const convertOld = (input: string) => {
return input.toLowerCase();
};
const convertNew = (input: string) => {
return input.toUpperCase();
};
const result = featureToggle("feature-convert-new")
? convertNew("Hello, world")
: convertOld("Hello, world");
console.log(result);
Codemod для удаления указанного feature toggle работает корректно, и после его выполнения исходный код должен выглядеть так:
const convertNew = (input: string) => {
return input.toUpperCase();
};
const result = convertNew("Hello, world");
console.log(result);
Однако, помимо удаления логики feature toggle, необходимо выполнить дополнительные задачи:
Удалить неиспользуемую функцию
convertOld
.Очистить импорт
featureToggle
, если он больше не используется.
Конечно, можно написать один большой codemod, который выполнит все эти изменения за один проход и протестировать его целиком. Однако более поддерживаемый подход — работать с codemod так же, как с продакшен-кодом: разбить задачу на меньшие, независимые части, как при рефакторинге.
Разбивка на части
Мы можем разделить большое преобразование на несколько отдельных codemods и затем объединить их. Преимущество такого подхода в том, что каждое преобразование можно тестировать отдельно, покрывая разные сценарии без взаимного влияния. Кроме того, это позволяет переиспользовать и комбинировать их для различных задач.
Например, можно выделить такие преобразования:
Преобразование для удаления конкретного feature toggle.
Очистка неиспользуемых импортов.
Удаление неиспользуемых объявлений функций.
Объединив эти преобразования, можно построить цепочку трансформаций, где каждый шаг выполняет свою задачу:
import { removeFeatureToggle } from "./remove-feature-toggle";
import { removeUnusedImport } from "./remove-unused-import";
import { removeUnusedFunction } from "./remove-unused-function";
import { createTransformer } from "./utils";
const removeFeatureConvertNew = removeFeatureToggle("feature-convert-new");
const transform = createTransformer([
removeFeatureConvertNew,
removeUnusedImport,
removeUnusedFunction,
]);
export default transform;
Удаление toggle feature-convert-new.
Очистка неиспользуемых импортов.
Удаление функции convertOld, так как она больше не используется.
Можно также выделить дополнительные codemods и комбинировать их в разном порядке в зависимости от желаемого результата.
Функция createTransformer
Реализация createTransformer
проста и понятна. Это функция высшего порядка, которая принимает список небольших функций-трансформаций, поочерёдно применяет их к AST и затем конвертирует модифицированное AST обратно в исходный код.
import { API, Collection, FileInfo, JSCodeshift, Options } from "jscodeshift";
type TransformFunction = { (j: JSCodeshift, root: Collection): void };
const createTransformer =
(transforms: TransformFunction[]) =>
(fileInfo: FileInfo, api: API, options: Options) => {
const j = api.jscodeshift;
const root = j(fileInfo.source);
transforms.forEach((transform) => transform(j, root));
return root.toSource(options.printOptions || { quote: "single" });
};
export { createTransformer };
Например, можно создать codemod, который встраивает выражения, присваивающие результат вызова feature toggle в переменную, чтобы на следующих этапах обработки не пришлось учитывать такие случаи:
const shouldEnableNewFeature = featureToggle('feature-convert-new');
if (!shouldEnableNewFeature && someOtherLogic) {
//...
}
Становится таким:
if (!featureToggle('feature-convert-new') && someOtherLogic) {
//...
}
Со временем можно собрать набор переиспользуемых небольших преобразований, что значительно упростит обработку сложных пограничных случаев. Этот подход оказался крайне эффективным при рефакторинге компонентов дизайн-системы. Например, после рефакторинга одного пакета (например, компонента Button) у нас уже были определены переиспользуемые codemods, такие как:
Добавление комментариев в начале функций.
Удаление устаревших пропсов.
Переименование импортов, если пакет уже импортирован выше.
Каждое из этих небольших преобразований можно тестировать и использовать отдельно или объединять для более сложных трансформаций, что значительно ускоряет последующие изменения в коде. В результате работа по рефакторингу стала более эффективной, а созданные codemods теперь применимы как для внутренних, так и для внешних кодовых баз React.
Поскольку каждое преобразование относительно изолированно, можно настраивать и оптимизировать их без влияния на другие codemods. Например, можно переписать одно из преобразований для повышения производительности — например, сократить количество проходов по AST. А благодаря полному покрытию тестами такие изменения можно вносить безопасно и уверенно.
Codemods в других языках
Хотя рассмотренные примеры сосредоточены на JavaScript и JSX с использованием jscodeshift, codemods можно применять и в других языках программирования. Например, JavaParser предлагает аналогичный механизм для Java, позволяя рефакторить код с помощью AST-манипуляций.
Использование JavaParser в кодовой базе на Java
JavaParser полезен для внесения критических изменений в API или автоматизированного рефакторинга больших Java-проектов.
Допустим, у нас есть следующий код в FeatureToggleExample.java
, который проверяет feature-convert-new
и выполняет соответствующую ветку:
public class FeatureToggleExample {
public void execute() {
if (FeatureToggle.isEnabled("feature-convert-new")) {
newFeature();
} else {
oldFeature();
}
}
void newFeature() {
System.out.println("New Feature Enabled");
}
void oldFeature() {
System.out.println("Old Feature");
}
}
Можно определить visitor
, который будет находить if-выражения, проверяющие FeatureToggle.isEnabled
, и заменять их на соответствующую true-ветку — аналогично тому, как мы реализовывали codemod для feature toggle в JavaScript.
// Visitor to remove feature toggles
class FeatureToggleVisitor extends VoidVisitorAdapter<Void> {
@Override
public void visit(IfStmt ifStmt, Void arg) {
super.visit(ifStmt, arg);
if (ifStmt.getCondition().isMethodCallExpr()) {
MethodCallExpr methodCall = ifStmt.getCondition().asMethodCallExpr();
if (methodCall.getNameAsString().equals("isEnabled") &&
methodCall.getScope().isPresent() &&
methodCall.getScope().get().toString().equals("FeatureToggle")) {
BlockStmt thenBlock = ifStmt.getThenStmt().asBlockStmt();
ifStmt.replace(thenBlock);
}
}
}
}
Этот код использует паттерн Visitor, реализованный с помощью JavaParser, для обхода и изменения AST. Класс FeatureToggleVisitor
находит if-выражения, в которых вызывается FeatureToggle.isEnabled()
, и заменяет их на true-ветку.
Можно также определить visitor, который будет находить неиспользуемые методы и удалять их:
class UnusedMethodRemover extends VoidVisitorAdapter<Void> {
private Set<String> calledMethods = new HashSet<>();
private List<MethodDeclaration> methodsToRemove = new ArrayList<>();
// Собираем все вызванные методы
@Override
public void visit(MethodCallExpr n, Void arg) {
super.visit(n, arg);
calledMethods.add(n.getNameAsString());
}
// Собираем методы для удаления, если они не были вызваны
@Override
public void visit(MethodDeclaration n, Void arg) {
super.visit(n, arg);
String methodName = n.getNameAsString();
if (!calledMethods.contains(methodName) && !methodName.equals("main")) {
methodsToRemove.add(n);
}
}
// После посещения, удаляем неиспользуемые методы
public void removeUnusedMethods() {
for (MethodDeclaration method : methodsToRemove) {
method.remove();
}
}
}
Этот visitor, UnusedMethodRemover
, позволяет обнаруживать и удалять неиспользуемые методы. Он отслеживает вызванные методы в calledMethods
, проверяет каждое объявление метода и, если метод не используется и не является main
, добавляет его в список methodsToRemove
. После обработки всех методов он удаляет неиспользуемые методы из AST.
Композиция Java Visitors
Эти visitor-ы можно объединять в цепочку преобразований и применять к кодовой базе следующим образом:
public class FeatureToggleRemoverWithCleanup {
public static void main(String[] args) {
try {
String filePath = "src/test/java/com/example/Example.java";
CompilationUnit cu = StaticJavaParser.parse(new FileInputStream(filePath));
// Применяем трансформации
FeatureToggleVisitor toggleVisitor = new FeatureToggleVisitor();
cu.accept(toggleVisitor, null);
UnusedMethodRemover remover = new UnusedMethodRemover();
cu.accept(remover, null);
remover.removeUnusedMethods();
// Записываем изменённый код обратно в файл
try (FileOutputStream fos = new FileOutputStream(filePath)) {
fos.write(cu.toString().getBytes());
}
System.out.println("Code transformation completed successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
}
Каждый visitor представляет собой отдельное преобразование, а паттерн Visitor в JavaParser упрощает их комбинирование.
OpenRewrite
Другим популярным инструментом для Java-проектов является OpenRewrite. Он использует альтернативный формат представления дерева исходного кода — Lossless Semantic Trees (LSTs), который содержит более детальную информацию, чем традиционные AST, применяемые в JavaParser или jscodeshift.
Если AST ориентирован на синтаксическую структуру, LST анализирует как синтаксис, так и семантический смысл кода, что делает преобразования более точными и гибкими.
OpenRewrite также предлагает широкую экосистему готовых сценариев рефакторинга для таких задач, как:
Миграция фреймворков
Исправление уязвимостей безопасности
Соблюдение единообразия стиля
Этот набор стандартных сценариев экономит время разработчиков, позволяя масштабировать изменения без необходимости писать собственные codemods.
Для более сложных задач OpenRewrite позволяет создавать и распространять собственные сценарии, что делает его мощным и гибким инструментом. В настоящее время он активно используется в Java-сообществе и постепенно адаптируется для других языков благодаря своей расширяемости и сообществу.
Различия между OpenRewrite, JavaParser и jscodeshift
Главное отличие OpenRewrite от JavaParser и jscodeshift заключается в подходе к трансформациям кода:
OpenRewrite использует Lossless Semantic Trees (LSTs), сохраняя как синтаксическую, так и семантическую структуру кода. Это делает его более точным инструментом для рефакторинга.
JavaParser и jscodeshift опираются на традиционные AST, которые фокусируются на синтаксисе, но могут не учитывать тонкости семантики.
OpenRewrite предоставляет библиотеку готовых сценариев рефакторинга, что значительно упрощает автоматизацию изменений по всей кодовой базе.
Другие инструменты для codemods
Хотя jscodeshift и OpenRewrite являются мощными инструментами, существуют альтернативы, которые могут быть полезны в зависимости от экосистемы и конкретных потребностей.
Hypermod
Hypermod использует искусственный интеллект для автоматизации процесса написания codemods. Вместо того чтобы вручную разрабатывать логику codemod, разработчики могут описать желаемое преобразование на простом английском языке, и Hypermod автоматически сгенерирует codemod с использованием jscodeshift. Это делает создание codemods более доступным, даже для разработчиков, не знакомых с манипуляцией AST.
Вы можете создавать, тестировать и развёртывать codemod в любом репозитории, подключённом к Hypermod. Инструмент может выполнить codemod и создать пул-реквест с предложенными изменениями, позволяя вам просмотреть и принять их. Такая интеграция значительно упрощает весь процесс — от разработки codemod до его развёртывания.
Codemod.com
Codemod.com — это сообщество разработчиков, где можно искать, публиковать и использовать готовые codemods. Если вам нужен конкретный codemod для типовой задачи рефакторинга или миграции, вы можете найти уже существующие codemods. Также вы можете опубликовать созданные вами codemods, чтобы помочь другим разработчикам в сообществе.
Если вы выполняете миграцию API и вам требуется codemod для её автоматизации, Codemod.com может сэкономить вам время. Он предоставляет готовые codemods для множества стандартных преобразований, уменьшая необходимость писать их с нуля.
Выводы
Codemods — мощные инструменты, позволяющие автоматизировать изменения кода и упростить:
актуализацию API,
рефакторинг устаревшего кода,
поддержание единообразия в больших кодовых базах с минимальными ручными правками.
Используя jscodeshift, Hypermod или OpenRewrite, можно оптимизировать как мелкие синтаксические изменения, так и масштабный рефакторинг компонентов, значительно улучшая качество и поддерживаемость кода.
Однако у codemods есть и сложности. Одна из ключевых проблем — обработка пограничных случаев, особенно в разнообразных кодовых базах. Различия в стиле кодирования, псевдонимы импортов или нестандартные паттерны могут привести к ошибкам, которые codemods не смогут обработать автоматически.
Такие пограничные случаи требуют тщательного планирования, полного покрытия тестами и, в некоторых случаях, ручного вмешательства для повышения точности.
Чтобы повысить эффективность codemods, важно разбивать сложные преобразования на более мелкие, тестируемые шаги и использовать инструменты стандартизации кода, где это возможно. Codemods могут быть очень эффективными, но их успешность зависит от грамотного проектирования и понимания ограничений, с которыми они могут столкнуться в более сложных или разнородных кодовых базах.
В завершение приглашаем всех, кому интересна тема автоматизации тестирования при помощи JavaScript, на открытые уроки:
5 февраля: «Тестирование API: JSON и валидация https запросов». Рассмотрим требования для валидации запросов и ответов API. Также разберем JSON Schema и автоматизируем процесс валидации JSON. Записаться
11 февраля: «Первый UI тест с использованием Cypress: Пошаговое руководство». На этом уроке рассмотрим основы Cypress и научимся применять его для написания автотестов. Записаться
Список всех бесплатных онлайн-занятий, которые проводят преподаватели-практики в рамках набора на курсы, можно посмотреть в календаре.