Практическое руководство по началу работы с абстрактным синтаксическим деревом AST

JavaScript

что такое АСТ

Абстрактное синтаксическое дерево, или сокращенно AST, представляет собой абстрактное представление грамматической структуры исходного кода. Он представляет синтаксическую структуру языка программирования в виде дерева, и каждый узел в дереве представляет собой структуру в исходном коде.

В чем польза АСТ

AST широко используется, например:

  • Подсказки редактора об ошибках, форматирование кода, подсветка кода, автодополнение кода;
  • elint,pretiierпроверка кода на наличие ошибок или стиля;
  • webpackпройти черезbabelперевестиjavascriptграмматика;

И если вы хотите понять, как компилируется и выполняется js, вам нужно понимать AST.

Как генерируется АСТ

Первым шагом, выполняемым js, является чтение потока символов в файле js, а затем его генерация с помощью лексического анализа.token, а затем сгенерировать AST через синтаксический анализатор и, наконец, сгенерировать машинный код для выполнения.

Весь процесс анализа в основном делится на следующие два этапа:

  • Сегментация слов: разбивает всю строку кода на массив минимальных синтаксических единиц.
  • Синтаксический анализ: установление и анализ отношений между грамматическими единицами на основе сегментации слов.

JS Parser — это парсер js, который может преобразовывать исходный код js в AST. Общие парсеры включают esprima, traceur, acorn, shift и т. д.

лексический анализ

Лексический анализ, также известный как сканер, просто вызывает метод next(), считывает символы один за другим, а затем сравнивает их с определенными ключевыми символами JavaScript для создания соответствующего токена. Токен представляет собой неделимую наименьшую единицу:

Например, три символа var могут использоваться только целиком и не могут быть разделены семантически, поэтому это токен.

В лексическом анализаторе каждое ключевое слово является Токеном, каждый идентификатор — Токеном, каждый оператор — Токеном, и каждый знак препинания — тоже Токеном. В дополнение к этому отфильтровываются комментарии и пробельные символы (переводы строк, пробелы, табуляции и т. д.) в исходной программе.

В конце концов, весь код будет разбит на список токенов (или одномерный массив).

Разбор

Синтаксический анализ преобразует токен из лексического анализа в структуру абстрактного синтаксического дерева с грамматическим значением. В то же время проверяется синтаксис, и если синтаксис неверен, выдается синтаксическая ошибка.

Сказав так много, давайте посмотрим, как выглядит фрагмент кода JavaScript после его преобразования в AST, и покажем это с помощью простой строки кода.

🌰 Пример 1

const fn = a => a;

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

Если перевести эту диаграмму человеческим языком, то: объявите переменную fn типа const, чтобы она указывала на выражение стрелочной функции, параметром которого является а, а тело функции также имеет значение а.

🌰 Пример 2

const fn = a => {
    let i = 1;
  return a + i;
};

Смотрим на корпус:

🌰 Пример 3

вызов функции

function test(){
  let a = 1;
  console.log(a)
}

В основном см.MemberExpression

Все приведенные выше скриншоты обработаны с помощью Acorn. Причина использования Acorn в том, что насколько я понимаю, Acorn признан самым быстрым в парсинге. Инструмент упаковки Webpack, который мы используем, также использует Acorn для Babel.

Свойства на снимке экрана выше являются частью AST, и эта структура содержит множество свойств.

  • Объявление переменной VariableDeclaration
  • Описание объявления переменной VariableDeclarator
  • Узел выражения выражения

Больше отображения недвижимости:

  1. можешь идтиAST explorerВы можете увидеть AST, полученный разными парсерами онлайн после парсинга кода js.
  2. Посмотреть все ESTrees на githubESTree
  3. Документация по внедрению свойствВведение в абстрактное синтаксическое дерево AST

Применение реальных боевых АСТ

тема

через вышеуказанноеconsole.logАСТ, давайте завершим звонокconsole.log(xx)При добавлении имени функции на передний план пользователь может легко увидеть, какая функция вызывается при печати.

举例

// 源代码
function getData() {
  console.log("data")
}

// --------------------

// 转化后代码
function getData() {
  console.log("getData", "data");
}

вводить

Во-первых, давайте представим инструменты, которые нам нужно использоватьBabel

  • @babel/parser: поставить код js -------->>>ASTабстрактное синтаксическое дерево;
  • @babel/traverseправильноASTУзлы просматриваются рекурсивно;
  • @babel/typesк конкретномуASTУзел изменен;
  • @babel/generator : ASTАбстрактное дерево синтаксиса ------->>> новый код js;

Зачем использовать babel?В основном потому, что им проще пользоваться (знаком только с этим 😭).

Входить@babel/parserВ начале официального сайта сообщается, что он использует Acorn для разбора js-кода в синтаксическое дерево AST (что указывает на то, что Acorn действительно лучше).

начать кодирование

  1. Создайте новый файл, чтобы открыть консоль для установки необходимых пакетов.
cnpm i @babel/parser @babel/traverse @babel/types @babel/generator -D
  1. Создайте файл js и напишите примерный макет следующим образом. Используйте AST
const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");

function compile(code) {
  // 1.parse 将代码解析为抽象语法树(AST)
  const ast = parser.parse(code);

  // 2,traverse 转换代码
  traverse.default(ast, {});

  // 3. generator 将 AST 转回成代码
  return generator.default(ast, {}, code);
}

const code = `
function getData() {
  console.log("data")
}
`;
const newCode = compile(code)

Используйте узел для запуска результата, потому что ничего не обрабатывается, на выходе исходный код,

Улучшить метод компиляции

function compile(code) {
  // 1.parse
  const ast = parser.parse(code);

  // 2,traverse
  const visitor = {
    CallExpression(path) {
      // 拿到 callee 数据
      const { callee } = path.node;
      // 判断是否是调用了 console.log 方法
      // 1. 判断是否是成员表达式节点,上面截图有详细介绍
      // 2. 判断是否是 console 对象
      // 3. 判断对象的属性是否是 log
      const isConsoleLog =
        types.isMemberExpression(callee) &&
        callee.object.name === "console" &&
        callee.property.name === "log";
      if (isConsoleLog) {
        // 如果是 console.log 的调用 找到上一个父节点是函数
        const funcPath = path.findParent(p => {
          return p.isFunctionDeclaration();
        });
        // 取函数的名称
        const funcName = funcPath.node.id.name;
        // 将名称通过 types 来放到函数的参数前面去
        path.node.arguments.unshift(types.stringLiteral(funcName));
      }
    }
  };
  // traverse 转换代码
  traverse.default(ast, visitor);

  // 3. generator 将 AST 转回成代码
  return generator.default(ast, {}, code);
}

Чистый код выглядит трудным для понимания.Ниже приведен формат данных, который я записал вышеприведенным path.node в файл для всеобщего обозрения.

{
  "type": "CallExpression",
  "start": 24,
  "end": 43,
  "loc": {
    "start": { "line": 3, "column": 2 },
    "end": { "line": 3, "column": 21 }
  },
  "callee": {
    "type": "MemberExpression",
    "start": 24,
    "end": 35,
    "loc": {
      "start": { "line": 3, "column": 2 },
      "end": { "line": 3, "column": 13 }
    },
    "object": {
      "type": "Identifier",
      "start": 24,
      "end": 31,
      "loc": {
        "start": { "line": 3, "column": 2 },
        "end": { "line": 3, "column": 9 },
        "identifierName": "console"
      },
      "name": "console"
    },
    "property": {
      "type": "Identifier",
      "start": 32,
      "end": 35,
      "loc": {
        "start": { "line": 3, "column": 10 },
        "end": { "line": 3, "column": 13 },
        "identifierName": "log"
      },
      "name": "log"
    },
    "computed": false
  },
  "arguments": [
    {
      "type": "StringLiteral",
      "start": 36,
      "end": 42,
      "loc": {
        "start": { "line": 3, "column": 14 },
        "end": { "line": 3, "column": 20 }
      },
      "extra": { "rawValue": "data", "raw": "'data'" },
      "value": "data"
    }
  ]
}

Мы удаляем ненужные атрибуты информации о местоположении (start, end, loc), и код будет понятен с первого взгляда при сравнении данных.

снова запустить файл

Отлично, вызовите метод console.log с именем функции, добавленным перед параметром метода, готово! !

Для удобства всех ниже приведен полный код

const generator = require("@babel/generator");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse");
const types = require("@babel/types");
const fs = require("fs");


function compile(code) {
  // 1.parse
  const ast = parser.parse(code);

  // 2,traverse
  const visitor = {
    CallExpression(path) {
      const { callee } = path.node;
      const isConsoleLog =
        types.isMemberExpression(callee) &&
        callee.object.name === "console" &&
        callee.property.name === "log";
      if (isConsoleLog) {
        const funcPath = path.findParent(p => {
          return p.isFunctionDeclaration();
        });
        const funcName = funcPath.node.id.name;
        fs.writeFileSync("./funcPath.json", JSON.stringify(funcPath.node), err => {
          if (err) throw err;
          console.log("写入成功");
        });
        path.node.arguments.unshift(types.stringLiteral(funcName));
      }
    }
  };
  traverse.default(ast, visitor);

  // 3. generator
  return generator.default(ast, {}, code);
}

const code = `
function getData() {
  console.log('data')
}
`;
console.log(compile(code).code);

Видя это, если вы думаете, что проблем нет, я полагаю, что у вас есть четкое понимание AST и определенное понимание скомпилированного кода babel, поэтому вы не будете так незнакомы с babel, когда будете писать конфигурацию веб-пакета в будущем.

Суммировать

Чтобы быть совместимым с браузерами более ранних версий, мы обычно используем webpack для упаковки и компиляции нашего кода, чтобы сократить синтаксис ES6, такой как функции стрелок, до обычных функций. Изменение объявлений const и let на var и т. д. выполняется через AST, но процесс реализации более сложен и утончен. Но это все три:

  1. js грамматика разбирается в AST;
  2. Изменить АСТ;
  3. Преобразование AST в синтаксис js;

Наконец

Если у вас есть время, вы также можете попробовать некоторые распространенные преобразования кода, такие как стрелочные функции в обычные функции, после того, как закончите попытки, что может усилить ваше впечатление.

Во всей статье, если есть какие-либо ошибки или неточности, обязательно исправьте их, спасибо!

Ссылаться на