Оригинал: https://www.sitepoint.com/understanding-asts-building-babel-plugin/
В этой статье для перевода выбраны только важные части
введение в язык
Мы разработали плагин для преобразования обычных объектов и массивов в постоянные структуры данных.Mori
Код, который мы хотим написать, таков:
var foo = { a: 1 };
var baz = foo.a = 2;
foo.a === 1;
baz.a === 2;
Что вы хотите преобразовать:
var foo = mori.hashMap('a', 1);
var baz = mori.assoc(foo, 'a', 2);
mori.get(foo, 'a') === 1;
mori.get(baz, 'a') === 2;
Babel
Основной процесс обработки Babel состоит из трех частей:
Parse
Babylon Разбирать и понимать код Javascript
Transform
babel-traverseАнализировать и модифицировать AST
Generate
babel-generatorПреобразование дерева AST обратно в обычный код
Абстрактное синтаксическое дерево AST
Понимание AST — основа нашего следующего контента. Язык Javascript генерируется из цепочки строк, каждая из которых содержит некоторую визуальную семантическую информацию. Все это полезно для нас, потому что позволяет использовать совпадающие символы ([], {}, ()), пары символов ("", '') и отступы, чтобы лучше понять программу. Тогда это бессмысленно для компьютера. Для них каждый символ — это просто числовое значение в памяти, и они не могут использовать их, чтобы задавать высокоуровневые вопросы, такие как «сколько переменных в этом операторе?» Вместо этого нам нужно пойти на компромисс и найти способ сделать код программируемое то, что может понять компьютер.
выглядит как следующий код
var a =3;
a + 5
Разобранное дерево AST
Все AST начинаются с корневого узла Program, который содержит представление программы верхнего уровня. В этом примере у нас есть только два:
- VariableDeclaration, используемый для присвоения значения NumericLiteral идентификатору Identifier, равному 3.
- ExpressionStatement состоит из BinaryExpression, состоящего из идентификатора «a» идентификатора и оператора «+», и значения 5.
Хотя они состоят из простых блоков, размер ast означает, что они довольно сложные, особенно для важных проектов. Вместо того, чтобы пытаться понять AST напрямую, мы можем использоватьastexplorer.net, Веб-сайт позволяет нам вводить код Javascript слева, а AST будет выводиться справа. Мы будем использовать этот инструмент, чтобы понять код и поэкспериментировать с ним.
Для стабильности Babel выберите в качестве интерпретатора "babylon6".
Setup
Убедитесь, что вы используете node и npm для установки. Создайте файл проекта, создайте файл package.json и установите следующие зависимости
mkdir moriscript && cd moriscript
npm init -y
npm install --save-dev babel-core
Мы создаем файловый плагин и экспортируем функцию по умолчанию
// moriscript.js
module.exports = function(babel) {
var t = babel.types;
return {
visitor: {
}
};
};
Babel предоставляет режим посетителя, который можно использовать для написания различных плагинов, операций вставки и удаления и т. д. для создания нового дерева AST.
// run.js
var fs = require('fs');
var babel = require('babel-core');
var moriscript = require('./moriscript');
// read the filename from the command line arguments
var fileName = process.argv[2];
// read the code from this file
fs.readFile(fileName, function(err, data) {
if(err) throw err;
// convert from a buffer to a string
var src = data.toString();
// use our plugin to transform the source
var out = babel.transform(src, {
plugins: [moriscript]
});
// print the generated code to screen
console.log(out.code);
});
МассивыМассив
Первая задача MoriScript — преобразовать Object и Array в их аналоги Mori: HashMapsh и Vector. Первое, что нам нужно преобразовать, это массив.
var bar = [1, 2, 3];
// should becom
var bar = mori.vector(1, 2, 3);
Скопируйте приведенный выше код в astexplorer и выделите массив [1,2,3], чтобы увидеть соответствующий узел AST.
Для удобочитаемости мы выбрали только узлы AST области данных:
// [1, 2, 3]
{
"type": "ArrayExpression",
"elements":[
{
"type": "NumericLiteral",
"value": 1
},
{
"type": "NumericLiteral",
"value": 2
},{
"type": "NumericLiteral",
"value": 3
}
]
}
И AST mori.vector(1,2,3) выглядит следующим образом:
{
"type": "CallExpression",
"callee": {
"type": "MemberExpression",
"object": {
"type": "Identifier",
"name": "mori"
},
"property":{
"type": "Identifier",
"name": "vector"
}
},
"arguments":[
{
"type": "NumericLiteral",
"value": 1
},
{
"type": "NumericLiteral",
"value": 2
},
{
"type": "NumericLiteral",
"value": 3
}
]
}
На визуализации вышеприведенного узла вы можете четко увидеть разницу между двумя деревьями.
Теперь мы можем четко видеть, что нам нужно заменить выражение верхнего уровня, но мы можем разделить числовое выражение между двумя деревьями.
Давайте начнем добавлять наше первое ArrayExpression к нашему посетителю:
module.exports = function(babel) {
var t = babel.types;
return {
visitor: {
ArrayExpression: function(path) {
}
}
};
};
Мы можем найти соответствующий тип выражения в документации по типам babel, в этом примере мы собираемся заменить ArrayExpression на CallExpression, мы можем сгенерировать t.callExpression(вызываемый, аргументы). Затем необходимо вызвать MemberExpression с помощью t.memberExpression(object, property).
ArrayExpression: function(path){
path.replaceWith(
t.callExpression(
t.memberExpression(t.identifier('mori'), t.identifier('vector')),
path.node.elements
)
)
}
Object
Следующий взгляд на Объект
var foo = { bar: 1};
var foo =mori.hashMap('bar',1);
Синтаксис объекта имеет структуру, аналогичную ArrayExpression.
Выделите mori.hashMap('bar', 1), чтобы получить:
{
"type": "ObjectExpression",
"properties": [
{
"type": "ObjectProperty",
"key": {
"type": "Identifier",
"name": "bar"
},
"value": {
"type": "NumericLiteral",
"value": 1
}
}
]
}
Визуализируйте результирующее дерево AST:
ObjectExpression: function(path){
var props = [];
path.node.properties.forEach(function(prop){
props.push(
t.stringLiteral(prop.key.name),
prop.value
);
});
path.replaceWith(
t.callExpression(
t.memberExpression(t.identifier('mori'), t.identifier('hasMap')),
props
)
)
}
Точно так же у нас есть CallExpression, окруженный MemberExpression. Код для Array очень похож, за исключением того, что нам нужно сделать немного сложнее, чтобы получить свойства и значения.
Assignment
foo.bar = 3;
mori.assoc(foo, 'bar', 3);
AssignmentExpression: function(path){
var lhs = path.node.left;
var rhs = path.node.right;
if(t.isMemberExpression(lhs)){
if(t.isIdentifier(lhs.property)){
lhs.property = t.stringLiteral(lhs.property.name);
}
path.replaceWith(
t.callExpression(
t.memberExpression(),
[lhs.object, lhs.property, rhs]
)
);
}
}
Membership
foo.bar;
mori.get(foo, 'bar');
MemberExpression: function(path){
if(t.isAssignmentExpression(path.parent)) return;
if(t.isIdentifier(path.node.property)){
path.node.property = t.stringLiteral(path.node.property.name)
}
path.replaceWith(
t.callExpression(
t.memberExpression(),
[path.node.object, path.node.property]
)
)
}
Одна из проблем заключается в том, что полученный mori.get снова будет MemberExpression, что приведет к рекурсии цикла.
// set a flag to recognize express has been tranverse
MemberExpression: function(path){
if(path.node.isClean) return ;
...
}
Babel может конвертировать только Javascript, который понимает парсер Babel.
Онлайн-разбор АСТ: https://astexplorer.net/