Синхронное самоБлог Цзяньшу
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.
- В первые дни были uglifyjs и esprima
- эспре, на основе эсприма, для эслинта,Introducing Espree, an Esprima alternative
- желудь, который утверждает, что имеет лучшую производительность, чем эсприма,Acorn: yet another JavaScript parser
- вавилон, из желудя, для вавилона
- babel-eslint, поддерживаемый командой babel для использования с ESLint,GitHub - babel/babel-eslint: ESLint using Babel as the parser.
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