Думаю, что рефакторинг проекта – тема, близкая каждому разработчику. Зачастую мы сталкиваемся с проблемами, когда нам перестает хватать средств IDE и регулярных выражений, и тогда на помощь приходят средства вроде тех, что описаны в этом посте. Codemod скрипты – это очень мощный инструмент. После его освоения станет ясно, что ваш рефакторинг уже никогда уже не будет прежним. Поэтому я перевел этот пост для нашего хабраблога. Желаю приятного прочтения.
Сопровождение кодовой базы может обернуться головной болью для любого разработчика, особенно когда дело касается JavaScript. В условиях постоянно меняющихся стандартов, синтаксиса и критических изменений сторонних пакетов поддерживать такой код очень непросто.
За последние годы JavaScript изменился до неузнаваемости. Развитие этого языка привело к тому, что была изменена даже простейшая задача по объявлению переменных. В ES6 появились let
и const
, стрелочные функции и множество других новшеств, каждое из которых приносит пользу разработчикам.
При создании и поддержке в рабочем состоянии кода, призванного выдерживать проверку временем, растёт нагрузка на разработчиков. Из этого поста вы узнаете, как можно автоматизировать задачи по широкомасштабному рефакторингу кода с использованием Codemod-скриптов и инструмента jscodeshift
, что позволит вам, например, легко обновлять свой код для использования новых возможностей языка.
Codemod
Codemod – это инструмент, разработанный Facebook для рефакторинга больших кодовых баз. Он позволяет разработчику реорганизовать большой объём кода за небольшой промежуток времени. Для небольших задач по рефакторингу вроде переименования класса или переменной разработчик использует IDE, такие изменения обычно затрагивают только один файл. Следующим инструментом для рефакторинга является глобальный поиск и замена. Часто он может работать с использованием сложных регулярных выражений. Но такой метод подходит не для всех случаев.
Codemod написан на Python, он принимает ряд параметров, включая выражения для поиска и замены.
codemod -m -d /code/myAwesomeSite/pages --extensions php,html
'<font *color="?(.*?)"?>(.*?)</font>'
'<span style="color: 1;">2</span>'
В приведённом примере мы заменяем <font>
на <span>
с использованием встроенного стиля для указания цвета. Первые два параметра – это флаги, указывающие на необходимость поиска нескольких совпадений (-m), и каталог для начала обработки (-d /code/myAwesomeSite/pages
). Мы также можем ограничить обрабатываемые расширения (-extensions php
, html
). Затем мы предоставляем выражения для поиска и замены. Если выражение для замены не указано, будет предложено ввести его во время выполнения. Инструмент работает нормально, но он очень похож на существующие инструменты для поиска и замены с использованием регулярных выражений.
jscodeshift
jscodeshift следующий в наборе инструментов для рефакторинга. Он также разработан Facebook и предназначен для обработки нескольких файлов Codemod-скриптом. Будучи модулем Node.js, jscodeshift предоставляет простой и удобный API, а «под капотом» использует Recast, являющийся инструментом преобразования AST-to-AST (Abstract Syntax Tree).
Recast
Recast – это модуль Node.js, который предоставляет интерфейс для парсинга и перегенерации JavaScript-кода. Он может анализировать код в виде строки и генерировать из него объекты, соответствующие структуре AST. Это позволяет разработчикам проверять код для таких шаблонов, как, например, объявление функций.
var recast = require("recast");
var code = [
"function add(a, b) {",
" return a + b",
"}"
].join("n");
var ast = recast.parse(code);
console.log(ast);
//output
{
"program": {
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "add",
"loc": {
"start": {
"line": 1,
"column": 9
},
"end": {
"line": 1,
"column": 12
},
"lines": {},
"indent": 0
}
},
...........
Как видно из примера, мы передаём строку кода с функцией, которая складывает два числа. Когда мы парсим строку и распечатываем получившийся объект, то можем видеть AST: видим FunctionDeclaration
, имя функции и т. д. Так как это просто JavaScript-объект, то мы можем изменить его как угодно. Затем можно вызвать функцию print для возврата обновлённой строки кода.
AST (Abstract Syntax Tree)
Как упоминалось ранее, Recast строит AST из строки кода. AST – это древовидное представление абстрактного синтаксиса исходного кода. Каждый узел дерева представляет собой конструкцию в исходном коде. ASTExplorer – это онлайн-инструмент, который поможет разобрать и понять дерево вашего кода.
С помощью ASTExplorer можно просмотреть AST простого примера кода. Объявим константу с именем foo и присвоим ей строковое значение ‘bar’
.
const foo = 'bar';
Это соответствует такому AST:
В массиве body есть ветвь VariableDeclaration
, которая содержит нашу константу. Все VariableDeclarations
имеют атрибут id
, который содержит важную информацию (имя и т. д.). Если бы мы создавали Codemod-скрипт для переименования всех экземпляров foo, то использовали бы этот атрибут и перебирали бы все экземпляры, чтобы изменить имя.
Установка и использование
Используя инструменты и приёмы, рассмотренные выше, мы можем воспользоваться всеми преимуществами jscodeshift. Поскольку, как мы знаем, он является модулем Node.js, можно установить его для проекта или глобально.
npm install -g jscodeshift
После установки мы можем использовать имеющиеся Codemod-скрипты совместно с jscodeshift
. Нужно предоставить некоторые параметры, которые укажут jscodeshift, чего мы хотим достичь. Базовый синтаксис – это вызов jscodeshift
с указанием пути к файлу или файлам, которые требуется преобразовать. Важным параметром является местоположение скрипта преобразования (-t)
: это может быть локальный файл или URL-адрес файла Codemod-скрипта. По умолчанию jscodeshift
ищет скрипт преобразования в файле transform.js в текущем каталоге.
Другими полезными параметрами являются пробный прогон (-d), который будет применять преобразование, но не станет обновлять файлы, и параметр -v, который выведет всю информацию о процессе преобразования. Скрипты преобразования – это Codemod-скрипты, простые модули JavaScript, которые экспортируют функцию. Эта функция принимает следующие параметры:
- fileInfo
- api
- options
Параметр fileInfo хранит всю информацию о текущем файле, включая путь и источник. Параметр api – это объект, который обеспечивает доступ к вспомогательным функциям jscodeshift
, таким как findVariableDeclarators
и renameTo
. Наконец, параметр – это опции, которые позволяют передавать параметры из командной строки в Codemod. Например, если мы хотим добавить версию кода ко всем файлам, то можем передать её через параметры командной строки jscodeshift -t myTransforms fileA fileB --codeVersion = 1.2
. В этом случае параметр options будет содержать {codeVersion: '1.2'}
.
Внутри функции, которую мы экспортируем, нужно вернуть преобразованный код в виде строки. Например, если у нас есть строка кода const foo = 'bar',
и мы хотим преобразовать её, заменив const foo
на const bar
, наш код будет выглядеть так:
export default function transformer(file, api) {
const j = api.jscodeshift;
return j(file.source)
.find(j.Identifier)
.forEach(path => {
j(path).replaceWith(
j.identifier('bar')
);
})
.toSource();
}
Здесь мы объединяем ряд функций в цепочку вызовов и в конце вызываем toSource()
для генерации преобразованной строки кода.
При возврате кода необходимо соблюдать некоторые правила. Возврат строки, отличной от входящей, считается успешным преобразованием. Если же строка такая же, как на входе, то преобразование считается неудачным, и, если ничего не будет возвращено, значит, оно не понадобилось. jscodeshift
использует эти результаты при обработке статистики по преобразованиям.
Существующие Codemod-скрипты
В большинстве случаев разработчикам не нужно писать собственный код – многие типовые действия по рефакторингу уже были превращены в Codemod-скрипты. Например, js-codemod no-vars
, который преобразуют все экземпляры var в let или const в зависимости от использования переменной (в let – если переменная будет изменена позже, в const – если переменная никогда не будет изменена).
js-codemod template-literals
заменяют конкатенацию строк шаблонными строками, например:
const sayHello = 'Hi my name is ' + name;
//after transform
const sayHello = `Hi my name is ${name}`;
Как пишутся Codemod-скрипты
Мы можем взять Codemod-скрипт no-vars
, упомянутый выше, и разбить код, чтобы увидеть, как работает сложный Codemod-скрипт.
const updatedAnything = root.find(j.VariableDeclaration).filter(
dec => dec.value.kind === 'var'
).filter(declaration => {
return declaration.value.declarations.every(declarator => {
return !isTruelyVar(declaration, declarator);
});
}).forEach(declaration => {
const forLoopWithoutInit = isForLoopDeclarationWithoutInit(declaration);
if (
declaration.value.declarations.some(declarator => {
return (!declarator.init && !forLoopWithoutInit) || isMutated(declaration, declarator);
})
) {
declaration.value.kind = 'let';
} else {
declaration.value.kind = 'const';
}
}).size() !== 0;
return updatedAnything ? root.toSource() : null;
Этот код является ядром скрипта no-vars. Во-первых, фильтр запускается на всех переменных VariableDeclaration
, включая var
, let
и const
, а возвращает только объявления var, которые передаются во второй фильтр, вызывающий пользовательскую функцию isTruelyVar
. Она используется для определения характера var (например, var внутри замыкания, или объявляется дважды, или объявление функции, которое может быть поднято) и покажет, безопасно ли делать преобразование этой var. Каждая var, которую возвращает фильтр isTruelyVar
, обрабатывается в цикле foreach.
Внутри цикла выполняется проверка, находится ли var внутри цикла, например:
for(var i = 0; i < 10; i++) {
doSomething();
}
Чтобы определить, находится ли var внутри цикла, можно проверить родительский тип.
const isForLoopDeclarationWithoutInit = declaration => {
const parentType = declaration.parentPath.value.type;
return parentType === 'ForOfStatement' || parentType === 'ForInStatement';
};
Если var
находится внутри цикла и не меняется, то её можно заменить на const
. Проверка может быть выполнена путём фильтрации по узлам var AssignmentExpression
и UpdateExpression. AssignmentExpression
покажет, где и когда var
была инициализирована, например:
var foo = 'bar';
UpdateExpression покажет, где и когда var была обновлена, например:
var foo = 'bar';
foo = 'Foo Bar'; //Updated
Если var
находится внутри цикла с изменением, используется let
, поскольку она может меняться после создания экземпляра. Последняя строка в скрипте проверяет, было ли что-нибудь изменено, и, если ответ положительный, возвращается новый исходный код для файла. В противном случае возвращается null
, сообщающий, что обработка данных не выполнялась. Полный код для этого Codemod-скрипта можно найти здесь.
Команда Facebook также добавила несколько Codemod-скриптов для обновления синтаксиса React и обработки изменений в React API. Например, react-codemod sort-comp
, который сортирует методы жизненного цикла React для соответствия правилу ESlint sort-comp.
Последним популярным Codemod-скриптом React является React-PropTypes-to-prop-types
, помогающий справиться с недавним изменением React, в результате которого разработчикам требуется установить prop-types
для продолжения использования PropTypes
в компонентах React версии 16. Это отличный пример использования Codemod-скриптов. Метод использования PropTypes
не увековечен в камне.
Верны все следующие примеры.
Импортируем React и получаем доступ к PropTypes
из импорта по умолчанию:
import React from 'react';
class HelloWorld extends React.Component {
static propTypes = {
name: React.PropTypes.string,
}
.....
Импортируем React и делаем именованный импорт для PropTypes:
import React, { PropTypes, Component } from 'react';
class HelloWorld extends Component {
static propTypes = {
name: PropTypes.string,
}
.....
И еще один вариант для для stateless
компонента:
import React, { PropTypes } from 'react';
const HelloWorld = ({name}) => {
.....
}
HelloWorld.propTypes = {
name: PropTypes.string
};
Наличие трёх способов реализации одного и того же решения затрудняет применение регулярного выражения для поиска и замены. Если бы в нашем коде были все три варианта, мы могли бы легко перейти к новому паттерну PropTypes, выполнив:
jscodeshift src/ -t transforms/proptypes.js
В этом примере мы взяли PropTypes Codemod-скрипт из репозитория react-codemod
и добавили его в каталог transforms
в нашем проекте. Скрипт добавит import PropTypes from 'prop-types
'; для каждого файла и заменит все React.PropTypes
на PropTypes
.
Выводы
Facebook начал внедрять поддержку кода, позволяя разработчикам адаптироваться под её постоянно меняющиеся API и практические подходы работы с кодом. Javascript Fatigue
стала большой проблемой, и, как мы увидели, инструменты, помогающие в процессе обновления существующего кода, во многом способствуют её решению.
В мире серверной разработки программисты регулярно создают сценарии миграции для поддержания баз данных в актуальном состоянии и обеспечения осведомлённости пользователей об их последних версиях. Создатели JavaScript-библиотек могли бы предоставлять Codemod-скрипты в качестве таких сценариев миграции, чтобы при выпуске новых версий с критическими изменениями можно было легко обработать свой код для обновления.
Наличие Codemod-скрипта, запускаемого автоматически при установке или обновлении, может ускорить процесс и повысить доверие потребителей. Кроме того, включение такого скрипта в процесс выпуска релизов не только было бы полезно для потребителей, но и уменьшило бы расходы на сопровождающие обновления примеры и руководства.
В этом посту мы рассмотрели природу Cdemod-скриптов и jscodeshift
и то, как быстро они могут обновлять сложный код. Начав с Codemod и перейдя к таким инструментам, как ASTExplorer
и jscodeshift
, можно научиться создавать Codemod-скрипты в соответствии со своими потребностями. А наличие широкого спектра готовых модулей позволяет разработчикам активно продвигать технологию в массы.
Примечание переводчика: на эту тему было интересное выступление на JSConfEU.
Автор: alxgutnikov