
Обзор
Для начала давайте взглянем на то, чего мы добьёмся, добравшись до конца этого материала:
// конструкция '@@' оснащает функцию `foo` возможностями каррирования
function @@ foo(a, b, c) {
return a + b + c;
}
console.log(foo(1, 2)(3)); // 6
Мы собираемся реализовать синтаксическую конструкцию
@@
, которая позволяет каррировать функции. Этот синтаксис похож на тот, что используется для создания функций-генераторов, но в нашем случае вместо знака *
между ключевым словом function
и именем функции размещается последовательность символов @@
. В результате при объявлении функций можно использовать конструкцию вида function @@ name(arg1, arg2)
.В вышеприведённом примере при работе с функцией
foo
можно воспользоваться её частичным применением. Вызов функции foo
с передачей ей такого количества параметров, которое меньше чем количество необходимых ей аргументов, приведёт к возврату новой функции, способной принять оставшиеся аргументы:foo(1, 2, 3); // 6
const bar = foo(1, 2); // (n) => 1 + 2 + n
bar(3); // 6
Я выбрал именно последовательность символов
@@
потому, что в именах переменных нельзя использовать символ @
. Это значит, что синтаксически корректной окажется и конструкция вида function@@foo(){}
. Кроме того, «оператор» @
применяется для функций-декораторов, а мне хотелось использовать что-то совершенно новое. В результате я и выбрал конструкцию @@
.Для того чтобы добиться поставленной цели, нам нужно выполнить следующие действия:
- Создать форк парсера Babel.
- Создать собственный плагин Babel для трансформации кода.
Выглядит как нечто невозможное?
На самом деле, ничего страшного тут нет, мы вместе всё подробно разберём. Я надеюсь, что вы, когда это дочитаете, будете мастерски владеть тонкостями Babel.
Создание форка Babel
Зайдите в репозиторий Babel на GitHub и нажмите на кнопку
Fork
, которая находится в левой верхней части страницы.
Создание форка Babel (изображение в полном размере)
И, кстати, если только что вы впервые создали форк популярного опенсорсного проекта — примите поздравления!
Теперь клонируйте форк Babel на свой компьютер и подготовьте его к работе.
$ git clone https://github.com/tanhauhau/babel.git
# set up
$ cd babel
$ make bootstrap
$ make build
Сейчас позвольте мне в двух словах рассказать об организации репозитория Babel.
Babel использует монорепозиторий. Все пакеты (например —
@babel/core
, @babel/parser
, @babel/plugin-transform-react-jsx
и так далее) расположены в папке packages/
. Выглядит это так:- doc
- packages
- babel-core
- babel-parser
- babel-plugin-transform-react-jsx
- ...
- Gulpfile.js
- Makefile
- ...
Отмечу, что в Babel для автоматизации задач используется Makefile. При сборке проекта, выполняемой командой
make build
, в качестве менеджера задач используется Gulp.Краткий курс по преобразованию кода в AST
Если вы не знакомы с такими понятиями, как «парсер» и «абстрактное синтаксическое дерево» (Abstract Syntax Tree, AST), то, прежде чем продолжать чтение, я настоятельно рекомендую вам взглянуть на этот материал.
Если очень кратко рассказать о том, что происходит при парсинге (синтаксическом анализе) кода, то получится следующее:
- Код, представленный в виде строки (тип
string
), выглядит как длинный список символов:f, u, n, c, t, i, o, n, , @, @, f, ...
- В самом начале Babel выполняет токенизацию кода. На этом шаге Babel просматривает код и создаёт токены. Например — нечто вроде
function, @@, foo, (, a, ...
- Затем токены пропускают через парсер для их синтаксического анализа. Здесь Babel, на основе спецификации языка JavaScript, создаёт абстрактное синтаксическое дерево.
Вот отличный ресурс для тех, кто хочет больше узнать о компиляторах.
Если вы думаете, что «компилятор» — это что-то очень сложное и непонятное, то знайте, что на самом деле всё не так уж и таинственно. Компиляция — это просто парсинг кода и создание на его основе нового кода, который мы назовём XXX. XXX-код может быть представлен машинным кодом (пожалуй, именно машинный код — это то, что первым всплывает в сознании большинства из нас при мысли о компиляторе). Это может быть JavaScript-код, совместимый с устаревшими браузерами. Собственно, одной из основных функций Babel является компиляция современного JS-кода в код, понятный устаревшим браузерам.
Разработка собственного парсера для Babel
Мы собираемся работать в папке
packages/babel-parser/
:- src/
- tokenizer/
- parser/
- plugins/
- jsx/
- typescript/
- flow/
- ...
- test/
Мы уже говорили о токенизации и о парсинге. Найти код, реализующий эти процессы, можно в папках с соответствующими именами. В папке
plugins/
содержатся плагины (подключаемые модули), которые расширяют возможности базового парсера и добавляют в систему поддержку дополнительных синтаксисов. Именно так, например, реализована поддержка jsx
и flow
.Давайте решим нашу задачу, воспользовавшись техникой разработки через тестирование (Test-driven development, TDD). По-моему, легче всего сначала написать тест, а потом, постепенно работая над системой, сделать так, чтобы этот тест выполнялся бы без ошибок. Такой подход особенно хорош при работе в незнакомой кодовой базе. TDD упрощает понимание того, в какие места кода нужно внести изменения для реализации задуманного функционала.
packages/babel-parser/test/curry-function.js
import { parse } from '../lib';
function getParser(code) {
return () => parse(code, { sourceType: 'module' });
}
describe('curry function syntax', function() {
it('should parse', function() {
expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();
});
});
Запуск теста для
babel-parser
можно выполнить так: TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only
. Это позволит увидеть ошибки:SyntaxError: Unexpected token (1:9)
at Parser.raise (packages/babel-parser/src/parser/location.js:39:63)
at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16)
at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18)
at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23)
at Parser.parseIdentifier (packages/babel-parser/src/parser/statement.js:1096:52)
Если вы обнаружите, что просмотр всех тестов занимает слишком много времени, то можете, для запуска нужного теста, вызвать
jest
напрямую:BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/curry-function.js
Наш парсер обнаружил 2 токена
@
, вроде бы совершенно невинных, там, где их быть не должно.Откуда я это узнал? Ответ на этот вопрос нам поможет найти использование режима мониторинга кода, запускаемого командой
make watch
.Просмотр стека вызовов приводит нас к packages/babel-parser/src/parser/expression.js, где выбрасывается исключение
this.unexpected()
.Добавим в этот файл пару команд логирования:
packages/babel-parser/src/parser/expression.js
parseIdentifierName(pos: number, liberal?: boolean): string {
if (this.match(tt.name)) {
// ...
} else {
console.log(this.state.type); // текущий токен
console.log(this.lookahead().type); // следующий токен
throw this.unexpected();
}
}
Как видно, оба токена — это
@
:TokenType {
label: '@',
// ...
}
Как я узнал о том, что конструкции
this.state.type
и this.lookahead().type
дадут мне текущий и следующий токены?Об этом я расскажу в разделе данного материала, посвящённом функциям
this.eat
, this.match
и this.next
.Прежде чем продолжать — давайте подведём краткие итоги:
- Мы написали тест для
babel-parser
. - Мы запустили тест с помощью
make test-only
. - Мы воспользовались режимом мониторинга кода с помощью
make watch
. - Мы узнали о состоянии парсера и вывели в консоль сведения о типе текущего токена (
this.state.type
).
А сейчас мы сделаем так, чтобы 2 символа
@
воспринимались бы не как отдельные токены, а как новый токен @@
, тот, который мы решили использовать для каррирования функций.Новый токен: «@@»
Для начала заглянем туда, где определяются типы токенов. Речь идёт о файле packages/babel-parser/src/tokenizer/types.js.
Тут можно найти список токенов. Добавим сюда и определение нового токена
atat
:packages/babel-parser/src/tokenizer/types.js
export const types: { [name: string]: TokenType } = {
// ...
at: new TokenType('@'),
atat: new TokenType('@@'),
};
Теперь давайте поищем то место кода, где, в процессе токенизации, создаются токены. Поиск последовательности символов
tt.at
в babel-parser/src/tokenizer
приводит нас к файлу: packages/babel-parser/src/tokenizer/index.js. В babel-parser
типы токенов импортируются как tt
.Теперь, в том случае, если после текущего символа
@
идёт ещё один @
, создадим новый токен tt.atat
вместо токена tt.at
:packages/babel-parser/src/tokenizer/index.js
getTokenFromCode(code: number): void {
switch (code) {
// ...
case charCodes.atSign:
// если следующий символ - это `@`
if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) {
// создадим `tt.atat` вместо `tt.at`
this.finishOp(tt.atat, 2);
} else {
this.finishOp(tt.at, 1);
}
return;
// ...
}
}
Если снова запустить тест — то можно заметить, что сведения о текущем и следующем токенах изменились:
// текущий токен
TokenType {
label: '@@',
// ...
}
// следующий токен
TokenType {
label: 'name',
// ...
}
Это уже выглядит довольно-таки неплохо. Продолжим работу.
Новый парсер
Прежде чем двигаться дальше — взглянем на то, как функции-генераторы представлены в AST.

AST для функции-генератора (изображение в полном размере)
Как видите, на то, что это — функция-генератор, указывает атрибут
generator: true
сущности FunctionDeclaration
.Мы можем применить аналогичный подход для описания функции, поддерживающей каррирование. А именно, мы можем добавить к
FunctionDeclaration
атрибут curry: true
.
AST для функции, поддерживающей каррирование (изображение в полном размере)
Собственно говоря, теперь у нас есть план. Займёмся его реализацией.
Если поискать в коде по слову
FunctionDeclaration
— можно выйти на функцию parseFunction
, которая объявлена в packages/babel-parser/src/parser/statement.js. Здесь можно найти строку, в которой устанавливается атрибут generator
. Добавим в код ещё одну строку:packages/babel-parser/src/parser/statement.js
export default class StatementParser extends ExpressionParser {
// ...
parseFunction<T: N.NormalFunction>(
node: T,
statement?: number = FUNC_NO_FLAGS,
isAsync?: boolean = false
): T {
// ...
node.generator = this.eat(tt.star);
node.curry = this.eat(tt.atat);
}
}
Если мы снова запустим тест, то нас будет ждать приятная неожиданность. Код успешно проходит тестирование!
PASS packages/babel-parser/test/curry-function.js
curry function syntax
? should parse (12ms)
И это всё? Что мы такого сделали, чтобы тест чудесным образом оказался пройденным?
Для того чтобы это выяснить — давайте поговорим о том, как работает парсинг. В процессе этого разговора, надеюсь, вы поймёте то, как подействовала на Babel строчка
node.curry = this.eat(tt.atat);
.Продолжение следует…
Уважаемые читатели! Используете ли вы Babel?


Комментарии (10)
slonopotamus
16.10.2019 21:52- Непонятно зачем помечать функции-которые-можно-каррировать вместо того чтобы каррировать ВСЕ функции
- А редактировать получившееся чудо теперь в чём? Откуда IDE узнают как распарсить @@?
andres_kovalev
17.10.2019 00:13ИМХО, для расширения функционала правильнее было бы реализовать новый плагин, нежели менять сам babel. Плагин можно сопровождать и распространять отдельно, что гораздо легче и реальнее, чем замержить такое в оригинальный репозиторий.
Senyaak
17.10.2019 10:50Зачем это делать? Для маленького проекта — это оверхед. Для больших проектов — не выгодно… Если отсутствует какойто синтаксис — лучше реализовывать его на постоянной основе, предложив внестив спецификацию языка, а не придумывать костыли… Костыли — это не есть хорошо
DarthVictor
17.10.2019 11:33Когда кто-то предлагает изменению в спецификацию языка, кому-то, часто самому предложившему, нужно это реализовывать в плагине к Babel. Также это может быть востребовано в различных фреймворках для реализации DSL, например в шаблонизаторах.
Senyaak
17.10.2019 11:48Смысл имплементировать нативные вещи в бабел? Это ересь — я не думаю что прежде чем попасть в пропозал — какието спред операторы или промисы писались под транспайлеры…
А фреймворк привязывать к баблу — вообще бессмысленно…DarthVictor
17.10.2019 12:57Смысл имплементировать нативные вещи в бабел?
У Babel'я есть плагины. Можно плагином.
А фреймворк привязывать к баблу — вообще бессмысленно…
Ну React как-то смог привязаться.
justboris
17.10.2019 17:03Вот здесь Babel официально декларирует, что они поддерживают все экспериментальные пропозалы, чтобы помочь комитету языка собрать фидбек и варианты использования от реальных пользователей.
Поэтому реализация новой фичи языка через бабель-плагин чтобы потестировать ее на реальных примерах – это нормальная затея.
Tarik02
17.10.2019 11:24Была идея разработать свой ЯП, синтаксис которого можно дополнять плагинами настолько, что можно было бы вводить конструкции типа json, xml и т.п. прямо в код. Идея была, но до реализации руки никак не доходят. Скорее всего такое сделать можно, но по моему это максимум Proof Of Concept.
webdevium
Форматирование исходного кода подкачало. Уберите теги, пожалуйста.
ru_vds Автор
Спасибо, поправили