AST in Modern JavaScript

внешний интерфейс JavaScript ECMAScript 6

Синхронное самоБлог Цзяньшу

What is AST

Что такое AST?AST — это аббревиатура от Abstract Syntax Tree (Абстрактное синтаксическое дерево). Три главных романа легендарного программиста — это принципы компиляции, графика и операционные системы.Если вы не играете с AST, этого недостаточно.Цель этой статьи — раскрыть применение AST в современных проектах JavaScript.

var a = 42
function addA(d){
  return a + d;
}

Код, написанный в соответствии с правилами грамматики, используется для того, чтобы сделать его читабельным и понятным разработчикам. Для таких инструментов, как компиляторы, он может понять абстрактное синтаксическое дерево.javascript-ast, вы можете визуально увидеть графическое синтаксическое дерево, сгенерированное исходным кодом

Генерация абстрактного синтаксического дерева должна пройти два этапа:

  • токенизировать
  • Семантический анализ (разбор)

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

Возьмите код var a = 42 в качестве примера, простое понимание, вы можете получить следующие результаты сегментации слов

[
    {type:'identifier',value:'var'},
    {type:'whitespace',value:' '},    
    {type:'identifier',value:'a'},
    {type:'whitespace',value:' '},
    {type:'operator',value:'='},
    {type:'whitespace',value:' '},
    {type:'num',value:'42'},
    {type:'sep',value:';'}
]

При фактическом использовании babylon6 для разбора этого кода результат сегментации слов будет

Сгенерированное абстрактное синтаксическое дерево

{
    "type":"Program",
    "body":[
        {
            "type":"VariableDeclaration",
            "kind":"var",
            "declarations":{
                "type":"VariableDeclarator",
                "id":{
                    "type":"Identifier",
                    "value":"a"
                },
                "init":{
                    "type":"Literal",
                    "value":42
                }
            }
        }
    ]
}

В сообществе существуют различные реализации синтаксического анализатора AST.

AST in ESLint

ESLint – это подключаемый модуль для проверки спецификаций написания JavaScript и составления отчетов. Он регулирует код, настраивая правила. Возьмем, к примеру, правило no-cond-assign. Когда это правило включено, присваивания в условных операторах не допускаются в коде. , Правило, позволяющее избегать неправильной записи суждений в виде присваиваний в условных операторах.

//check ths user's job title
if(user.jobTitle = "manager"){
  user.jobTitle is now incorrect
}

Инспекция ESLint основана на AST.В дополнение к этим встроенным правилам ESLint предоставляет нам API, который позволяет нам разрабатывать собственные плагины и пользовательские правила с использованием AST, сгенерированного из исходного кода.

module.exports = {
    rules: {
        "var-length": {
            create: function (context) {
                //规则实现
            }
        }
    }
};

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

Например, мы хотим реализовать правило, требующее, чтобы в операторе присваивания длина имени переменной превышала две цифры.

module.exports = {
    rules: {
        "var-length": {
            create: function (context) {
                return {
                    VariableDeclarator: node => {
                        if (node.id.name.length < 2) {
                            context.report(node, 'Variable names should be longer than 1 character');
                        }
                    }
                };
            }
        }
    }
};

Напишите package.json для этого плагина

{
    "name": "eslint-plugin-my-eslist-plugin",
    "version": "0.0.1",
    "main": "index.js",
    "devDependencies": {
        "eslint": "~2.6.0"
    },
    "engines": {
        "node": ">=0.10.0"
    }
}

При использовании в проекте после установки зависимостей через npm включить плагины и соответствующие правила в конфигурации

"plugins": [
    "my-eslint-plugin"
]

"rules": {
    "my-eslint-plugin/var-length": "warn"
}

С этими конфигурациями можно использовать вышеуказанные пользовательские плагины.

Иногда мы не хотим публиковать новые плагины, а хотим только написать локальные пользовательские правила, тогда мы можем сделать это с помощью пользовательских правил. Пользовательское правило примерно такое же, как и структура плагина, ниже приведено пользовательское правило, запрещающее использование в коде вызовов консольных методов.

const disallowedMethods = ["log", "info", "warn", "error", "dir"];
module.exports = {
    meta: {
        docs: {
            description: "Disallow use of console",
            category: "Best Practices",
            recommended: true
        }
    },
    create(context) {
        return {
            Identifier(node) {
                const isConsoleCall = looksLike(node, {
                    name: "console",
                    parent: {
                        type: "MemberExpression",
                        property: {
                            name: val => disallowedMethods.includes(val)
                        }
                    }
                });
                // find the identifier with name 'console'
                if (!isConsoleCall) {
                    return;
                }

                context.report({
                    node,
                    message: "Using console is not allowed"
                });
            }
        };
    }
};

AST in Babel

Babel — это инструмент компиляции для разработки с использованием функций синтаксиса JavaScript следующего поколения.Первоначально проект назывался 6to5, что означает преобразование синтаксиса ES6 в ES5. На данный момент Babel сформировал сильную экосистему.

Комментарии лидеров отрасли: Babel — это новый jQuery

Рабочий процесс Babel проходит три этапа: анализ, преобразование и генерация.В частности, как показано на рисунке ниже, на этапе анализа используется библиотека babylon для преобразования исходного кода в AST, а на этапе преобразования — различные плагины. -ins используются для преобразования кода. Преобразование JSX на рисунке преобразует React JSX в простой объект, а на этапе генератора инструмент генерации кода используется для преобразования AST в код.

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

import * as babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";

const code = `function square(n) {
    return n * n;
}`

const ast = babylon.parse(code);
traverse(ast,{
    enter(path){
        if(path.node.type === 'Identifier' && path.node.name === 'n'){
            path.node.name = 'x'
        }
    }
})
generate(ast,{},code)

Сценариев прямого использования этих API не так много, в проектах часто используются различные плагины Babel, такие какbabel-plugin-transform-remove-consoleПлагин умеет удалять все вызовы методов на консоль в коде.Основной код выглядит следующим образом

module.exports = function({ types: t }) {
  return {
    name: "transform-remove-console",
    visitor: {
      CallExpression(path, state) {
        const callee = path.get("callee");

        if (!callee.isMemberExpression()) return;

        if (isIncludedConsole(callee, state.opts.exclude)) {
          // console.log()
          if (path.parentPath.isExpressionStatement()) {
            path.remove();
          } else {
          //var a = console.log()
            path.replaceWith(createVoid0());
          }
        } else if (isIncludedConsoleBind(callee, state.opts.exclude)) {
          // console.log.bind()
          path.replaceWith(createNoop());
        }
      },
      MemberExpression: {
        exit(path, state) {
          if (
            isIncludedConsole(path, state.opts.exclude) &&
            !path.parentPath.isMemberExpression()
          ) {
          //console.log = func
            if (
              path.parentPath.isAssignmentExpression() &&
              path.parentKey === "left"
            ) {
              path.parentPath.get("right").replaceWith(createNoop());
            } else {
            //var a = console.log
              path.replaceWith(createNoop());
            }
          }
        }
      }
    }
  };

С помощью этого плагина следующие вызовы в программе могут быть преобразованы

console.log()
var a = console.log()
console.log.bind()
var b = console.log
console.log = func

//output
var a = void 0
(function(){})
var b = function(){}
console.log = function(){}

Метод работы вышеупомянутого плагина Babel аналогичен вышеупомянутому пользовательскому плагину/правилу ESLint.Когда инструмент проходит AST, сгенерированный исходным кодом, он выполняет соответствующие проверки в соответствии с указанным нами типом узла.

在我们开发插件时,是如何确定代码AST树形结构呢? годный к употреблениюAST explorerУдобно просмотреть соответствующую структуру AST, сгенерированную исходным кодом.

AST in Codemod

Codemod можно использовать, чтобы помочь вам автоматизировать изменения вашего кода в большой кодовой базе. jscodeshift — это инструмент JavaScript для запуска кодмодов, в основном опирающийся на две библиотеки инструментов recast и ast-types. recast предоставляет интерфейс AST в качестве синтаксического анализатора JavaScript, а ast-types предоставляет определения типов.

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

export default (fileInfo,api)=>{
    const j = api.jscodeshift;
    
    const root = j(fileInfo.source);
    
    const callExpressions = root.find(j.CallExpression,{
        callee:{
            type:'MemberExpression',
            object:{
                type:'Identifier',
                name:'console'
            }
        }
    });
    
    callExpressions.remove();
    
    return root.toSource();
}

Если вы хотите, чтобы код выглядел более лаконично, вы также можете использовать связанные вызовы API.

export default (fileInfo,api)=>{
    const j = api.jscodeshift;

    return j(fileInfo.source)
        .find(j.CallExpression,{
            callee:{
                type:'MemberExpression',
                object:{
                    type:'Identifier',
                    name:'console'
                }
            }
        })
        .remove()
        .toSource();
}

Узнав о jscodeshift, у меня сразу возник вопрос, а зачем нам нужен jscodeshift? Использование AST для преобразования кода, разве Babel не полностью готов?

После поиска с этим вопросом я обнаружил, что команда Babel отправила инструкции здесьbabel-core: add options for different parser/generator.

Как упоминалось выше, поток обработки Babel включает в себя три этапа: синтаксический анализ, преобразование и генерация. На этапе генерации кода Babel не заботится о формате сгенерированного кода, потому что сгенерированный скомпилированный код не предназначен для чтения разработчиками, а генерируется в каталог релиза для запуска, и код обычно сжимается во время этого процесса. .

Этот процесс также отражен в использовании команды Babel Форма команды, которую мы обычно используем, это

babel src -d dist

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

babel src -d src

В ходе этого процесса мы проверяем, какие изменения скрипт преобразования внес в исходный код, чтобы убедиться, что наше преобразование выполнено правильно. Для этого требуется, чтобы результат различия был читабельным, а при использовании Babel для непосредственного завершения вышеуказанного преобразования, при использовании git diff для вывода результата различия, результат различия будет беспорядочным и нечитаемым.

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

{
    "plugins":[
        "./plugins.js"
    ],
    "parserOpts":{
        "parser":"recast"
    },
    "generatorOpts":{
        "generator":"recast"
    }
}

Предположим, у нас есть следующий код, модифицируем режим импорта в коде через скрипт

import fs, {readFile} from 'fs'
import {resolve} from 'path'
import cp from 'child_process'

resolve(__dirname, './thing')

readFile('./thing.js', 'utf8', (err, string) => {
  console.log(string)
})

fs.readFile('./other-thing', 'utf8', (err, string) => {
  const resolve = string => string
  console.log(resolve())
})

cp.execSync('echo "hi"')

//转换为
import fs from 'fs';
import _path from 'path';
import cp from 'child_process'

_path.resolve(__dirname, './thing')

fs.readFile('./thing.js', 'utf8', (err, string) => {
  console.log(string)
})

fs.readFile('./other-thing', 'utf8', (err, string) => {
  const resolve = string => string
  console.log(resolve())
})

cp.execSync('echo "hi"')

Плагин plugin.js, который выполняет это преобразование,

module.exports = function(babel) {
  const { types: t } = babel
  // could just use https://www.npmjs.com/package/is-builtin-module
  const nodeModules = [
    'fs', 'path', 'child_process',
  ]

  return {
    name: 'node-esmodule', // not required
    visitor: {
      ImportDeclaration(path) {
        const specifiers = []
        let defaultSpecifier
        path.get('specifiers').forEach(specifier => {
          if (t.isImportSpecifier(specifier)) {
            specifiers.push(specifier)
          } else {
            defaultSpecifier = specifier
          }
        })
        const {node: {value: source}} = path.get('source')
        if (!specifiers.length || !nodeModules.includes(source)) {
          return
        }
        let memberObjectNameIdentifier
        if (defaultSpecifier) {
          memberObjectNameIdentifier = defaultSpecifier.node.local
        } else {
          memberObjectNameIdentifier = path.scope.generateUidIdentifier(source)
          path.node.specifiers.push(t.importDefaultSpecifier(memberObjectNameIdentifier))
        }
        specifiers.forEach(specifier => {
          const {node: {imported: {name}}} = specifier
          const {referencePaths} = specifier.scope.getBinding(name)
          referencePaths.forEach(refPath => {
            refPath.replaceWith(
              t.memberExpression(memberObjectNameIdentifier, t.identifier(name))
            )
          })
          specifier.remove()
        })
      }
    }
  }
}

Удалите и плюс установка Parseropts и GeneratorOpts запускается дважды, используйте команду git diff для вывода результата, вы можете увидеть значительную разницу

использовать переделку

не использовать переделку

AST in Webpack

Webpack – это инструмент для экологически чистой упаковки JavaScript. Его структура пакета представляет собой IIFE (функция немедленного выполнения).

(function(module){})([function(){},function(){}]);

Webpack также нуждается в поддержке AST в процессе упаковки: он использует библиотеку acorn для разбора исходного кода, генерации AST и извлечения зависимостей модулей.

Среди различных инструментов упаковки функция, предложенная Rollup и в настоящее время поддерживаемая Webpack, — это treeshaking. Treeshaking может удалить неиспользуемые модули из упакованного вывода, эффективно уменьшая размер пакета.

//math.js
export {doMath, sayMath}

const add = (a, b) => a + b
const subtract = (a, b) => a - b
const divide = (a, b) => a / b
const multiply = (a, b) => a * b

function doMath(a, b, operation) {
  switch (operation) {
    case 'add':
      return add(a, b)
    case 'subtract':
      return subtract(a, b)
    case 'divide':
      return divide(a, b)
    case 'multiply':
      return multiply(a, b)
    default:
      throw new Error(`Unsupported operation: ${operation}`)
  }
}

function sayMath() {
  return 'MATH!'
}

//main.js
import {doMath}
doMath(2, 3, 'multiply') // 6

В приведенном выше коде math.js выводит методы doMath и sayMath, и только метод doMath упоминается в main.js, используя функцию древовидной структуры Webpack, а также поддержку uglify, в выходном файле пакета вы можете удалить sayMath связанный код, выходная математика .js выглядит так

export {doMath}

const add = (a, b) => a + b
const subtract = (a, b) => a - b
const divide = (a, b) => a / b
const multiply = (a, b) => a * b

function doMath(a, b, operation) {
  switch (operation) {
    case 'add':
      return add(a, b)
    case 'subtract':
      return subtract(a, b)
    case 'divide':
      return divide(a, b)
    case 'multiply':
      return multiply(a, b)
    default:
      throw new Error(`Unsupported operation: ${operation}`)
  }
}

Дальнейший анализ вызовов в main.js, вызов doMath(2, 3, 'multiply') будет выполнять только ветвь doMath, некоторые вспомогательные методы, определенные в math.js, такие как сложение, вычитание, деление, на самом деле не нужны, теоретически оптимум math.js можно свести к

export {doMath}

const multiply = (a, b) => a * b

function doMath(a, b) {
  return multiply(a, b)
}

Основываясь на AST, более полный анализ покрытия кода должен быть в состоянии достичь вышеуказанных эффектов Это всего лишь идея, а не конкретная практика. Ссылаться наFaster JavaScript with SliceJS

Справочная статья