[Перевод] Понимание AST для создания плагина Babel

JavaScript Babel

Оригинал: 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 состоит из трех частей:

Babel 转换

Parse

Babylon Разбирать и понимать код Javascript

Transform

babel-traverseАнализировать и модифицировать AST

Generate

babel-generatorПреобразование дерева AST обратно в обычный код

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

Понимание AST — основа нашего следующего контента. Язык Javascript генерируется из цепочки строк, каждая из которых содержит некоторую визуальную семантическую информацию. Все это полезно для нас, потому что позволяет использовать совпадающие символы ([], {}, ()), пары символов ("", '') и отступы, чтобы лучше понять программу. Тогда это бессмысленно для компьютера. Для них каждый символ — это просто числовое значение в памяти, и они не могут использовать их, чтобы задавать высокоуровневые вопросы, такие как «сколько переменных в этом операторе?» Вместо этого нам нужно пойти на компромисс и найти способ сделать код программируемое то, что может понять компьютер.

выглядит как следующий код

var a =3;
a + 5

Разобранное дерево AST

Все AST начинаются с корневого узла Program, который содержит представление программы верхнего уровня. В этом примере у нас есть только два:

  1. VariableDeclaration, используемый для присвоения значения NumericLiteral идентификатору Identifier, равному 3.
  2. 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/