![Когда код это данные - 1 Когда код это данные - 1](https://www.pvsm.ru/images/2021/06/23/kogda-kod-eto-dannye.png)
«Представь, что люди как бы находятся в подземном жилище наподобие пещеры, где во всю её длину тянется широкий просвет. С малых лет у них на ногах и на шее оковы, так что людям не двинуться с места, и видят они только то, что у них прямо перед глазами, ибо повернуть голову они не могут из-за этих оков.»
© Платон «Государство», книга 7: Миф О Пещере
Время от времени мне пишут с просьбой помочь в написании кода, который меняет код (далее кодмод, от слов код и модификация - изменение) и сегодня я расскажу об этом нехитром процессе в новом формате, вдохновлённом диалогами Платона, он будет содержать вопросы обратившегося ко мне человека по поводу линтера нового поколения, и мои развёрнутые ответы.
Забегая вперед скажу, что результатом общения стал loader ESTrace, который при запуске может показать что-то вроде:
![Когда код это данные - 2 Когда код это данные - 2](https://www.pvsm.ru/images/2021/06/23/kogda-kod-eto-dannye-2.png)
Но об этом позже, а сейчас:
▍Следим за функциями
«Люди обращены спиной к свету, исходящему от огня, который горит далеко в вышине, а между огнём и узниками проходит верхняя дорога, ограждённая невысокой стеной вроде той ширмы, за которой фокусники помещают своих помощников, когда поверх ширмы показывают кукол.»
© Платон «Государство», книга 7: Миф О Пещере
Я хочу получать информацию о выполнении функций, самый простой вариант console.log('function name', arguments) мне подойдёт. Если получится добавить поддержку методов будет великолепно.
Узлы содержащие функции в Babel AST могут быть такими:
— FunctionDeclaration — объявление функции
function hello() {
return 'world';
}
— FunctionExpression — анонимная функция
hello(function(word) {
return `hello ${word}`;
});
— ArrowFunctionExpression - анонимная стрелочная функция
hello((word) => {
return `hello ${word}`;
});
— ClassMethod - метод класса
class Hello {
hello(word) {
return `hello ${word}`;
}
}
Для их поиска мы можем использовать Function, он объединяет в себе все перечисленные выше варианты.
Будем использовать Включитель и экспортировать функции:
- include, чтобы знать, что искать;
- fix, для изменения кода;
Таким образом, функция поиска:
module.exports.include = () => [
'Function',
];
Создавать узлы будем с помощью @babel/template, после чего добавим результат в начало функции:
const {template} = require('putout');
// самый простой способ создать узел
const buildLog = template(`console.log('NAME', arguments)`);
module.exports.fix = (path) => {
const {body} = path.node.body;
const NAME = path.node.id.name;
// добавляем в начало функции «console.log»
body.unshift(buildLog({
NAME,
}));
};
Соединив предыдущие две части, и улучшив разбор имени функции в соответствии с внутренней структурой, получим:
const {template} = require('putout');
// самый простой способ создать узел
const buildLog = template(`console.log('NAME', arguments)`);
// узлы, которые ищем
module.exports.include = () => [
'Function',
];
module.exports.fix = (path) => {
const {body} = path.node.body;
const NAME = getName(path);
// добавляем в начало функции "console.log"
body.unshift(buildLog({
NAME,
}));
};
// разбираем имя для вывода в логах
function getName(path) {
if (path.isClassMethod())
return path.node.key.name;
if (path.isFunctionDeclaration())
return path.node.id.name;
return '<undetermined>';
}
которая отрабатывает так (картинка кликабельная):
![Когда код это данные - 3 Когда код это данные - 3](https://www.pvsm.ru/images/2021/06/23/kogda-kod-eto-dannye-3.png)
▍Вносим неразбериху улучшения
«За этой стеной другие люди несут различную утварь, держа её так, что она видна поверх стены; проносят они и статуи, и всяческие изображения живых существ, сделанные из камня и дерева. При этом, как водится, одни из несущих разговаривают, другие молчат.»
© Платон «Государство», книга 7: Миф О Пещере
Отлично!
❒ Еще я понял что мне нужно логировать события входа в функцию и выхода из нее, меняя:function X() { console.log('hello') }
на
function X() { console.log('enter X') try { console.log('hello') } finally { console.log('exit X') } }
Буду рад помощи.
Для краткости и наглядности будем использовать бросающиеся в глаза сокращения:
const enterLog = buildLogEvent(name, '💣'); // вход
const exitLog = buildLogEvent(name, '💥'); // выход
const errorLog = buildLogEvent(name, '❌'); // ошибка
Еще нам нужно создать узел try-catch:
const buildTryCatch = template(`try {
BLOCK;
} catch(error) {
CATCH;
} finally {
FINALLY;
}
`);
// помещаем тело функции в try-catch
const bodyPath = path.get('body');
replaceWith(bodyPath, BlockStatement([buildTryCatch({
BLOCK: path.node.body.body,
CATCH: errorLog,
FINALLY: exitLog,
})]));
Строить лог будем таким образом, чтобы аргументы выводились как массив, а не объект:
const buildLog = template('console.log(`${«TYPE»} ${«NAME»}`, Array.from(arguments));');
const {template, types, operator} = require('putout');
const {replaceWith} = operator;
const {BlockStatement} = types;
// создаем узлы
const buildLog = template('console.log(`${"TYPE"} ${"NAME"}`, Array.from(arguments));');
const buildTryCatch = template(`try {
BLOCK;
} catch(error) {
CATCH;
} finally {
FINALLY;
}
`);
// узлы, которые ищем
module.exports.include = () => [
'Function',
];
module.exports.fix = (path) => {
const name = getName(path);
// создаем 3 вида событий
const enterLog = buildLogEvent(name, '💣');
const exitLog = buildLogEvent(name, '💥');
const errorLog = buildLogEvent(name, '❌');
// помещаем тело функции в try-catch
const bodyPath = path.get('body');
replaceWith(bodyPath, BlockStatement([buildTryCatch({
BLOCK: path.node.body.body,
CATCH: errorLog,
FINALLY: exitLog,
})]));
// добавляем в начало функции "console.log" с событием "enter"
bodyPath.node.body.unshift(enterLog);
};
// получаем имя для вывода в логах
function getName(path) {
if (path.isClassMethod())
return path.node.key.name;
if (path.isFunctionDeclaration())
return path.node.id.name;
return '<undetermined>';
}
// строим логер
function buildLogEvent(name, type) {
return buildLog({
NAME: name,
TYPE: type,
});
}
выглядит так (картинка кликабельная):
![Когда код это данные - 4 Когда код это данные - 4](https://www.pvsm.ru/images/2021/06/23/kogda-kod-eto-dannye-4.png)
▍Catch должен выбрасывать исключения
«Прежде всего разве ты думаешь, что, находясь в таком положении, люди что-нибудь видят, своё ли или чужое, кроме теней, отбрасываемых огнём на расположенную перед ними стену пещеры?
— Как же им видеть что-то иное, раз всю свою жизнь они вынуждены держать голову неподвижно?
— А предметы, которые проносят там, за стеной? Не то же ли самое происходит и с ними?
— То есть?
— Если бы узники были в состоянии друг с другом беседовать, разве, думаешь ты, не считали бы они, что дают названия именно тому, что видят?
— Непременно так.»© Платон «Государство», книга 7: Миф О Пещере
❒ Catch должен выбрасывать исключение, без этого функция будет отрабатывать неправильно.
А еще у стрелочных функций нет имен, возможно ли логировать их положение в файле?
Да это возможно, у каждого узла path есть node, а у него loc, в котором start. Из start достаем номер строки line:
function getName(path) {
const {line} = path.node.loc.start;
return `<anonymous:${line}>`;
}
А еще нам нужна функция, которая будет логировать + выбрасывать исключение:
const buildLogException = template('console.log(`${«TYPE»} ${«NAME»}: ${traceError.message}`); throw traceError');
function buildLogExceptionEvent(name) {
return buildLogException({
NAME: name,
TYPE: '',
});
}
Нет никакой необходимости выводить аргументы в каждом событии, поэтому делаем buildLog универсальным:
const buildLog = template('console.log(`${"TYPE"} ${"NAME"}`)');
const {template, types, operator} = require('putout');
const {replaceWith} = operator;
const {BlockStatement} = types;
// создаем узлы
const buildLog = template('console.log(`${"TYPE"} ${"NAME"}`)');
const buildLogEnter = template('console.log(`'💣' ${"NAME"}`, Array.from(arguments));');
const buildLogException = template('console.log(`${"TYPE"} ${"NAME"}: ${traceError.message}`); throw traceError');
const buildTryCatch = template(`try {
BLOCK;
} catch(traceError) {
CATCH;
} finally {
FINALLY;
}
`);
// узлы которые ищем
module.exports.include = () => [
'Function',
];
// исправляем
module.exports.fix = (path) => {
const name = getFunctionName(path);
// создаем 3 вида событий
const enterLog = buildLogEnter({
NAME: name,
});
const exitLog = buildLogEvent(name, '💥');
const errorLog = buildLogExceptionEvent(name);
// помещаем тело функции в try-catch
const bodyPath = path.get('body');
replaceWith(bodyPath, BlockStatement([buildTryCatch({
BLOCK: path.node.body.body,
CATCH: errorLog,
FINALLY: exitLog,
})]));
// помещаем лог в начало функции
bodyPath.node.body.unshift(enterLog);
};
function getFunctionName(path) {
if (path.isClassMethod())
return path.node.key.name;
if (path.isFunctionDeclaration())
return path.node.id.name;
const {line} = path.node.loc.start;
return `<anonymous:${line}>`;
}
function buildLogEvent(name, type) {
return buildLog({
NAME: name,
TYPE: type,
});
}
function buildLogExceptionEvent(name) {
return buildLogException({
NAME: name,
TYPE: '❌'',
});
}
выглядит так (картинка кликабельная):
![Когда код это данные - 5 Когда код это данные - 5](https://www.pvsm.ru/images/2021/06/23/kogda-kod-eto-dannye-5.png)
▍Выдох
«Когда с кого-нибудь из них снимут оковы, заставят его вдруг встать, повернуть шею, пройтись, взглянуть вверх — в сторону света, ему будет мучительно выполнять всё это, он не в силах будет смотреть при ярком сиянии на те вещи, тень от которых он видел раньше. И как ты думаешь, что он скажет, когда ему начнут говорить, что раньше он видел пустяки, а теперь, приблизившись к бытию и обратившись к более подлинному, он мог бы обрести правильный взгляд? Да ещё если станут указывать на ту или иную проходящую перед ним вещь и заставят отвечать на вопрос, что это такое? Не считаешь ли ты, что это крайне его затруднит и он подумает, будто гораздо больше правды в том, что он видел раньше, чем в том, что ему показывают теперь?
— Конечно, он так подумает.
— А если заставить его смотреть прямо на самый свет, разве не заболят у него глаза, и не отвернётся он поспешно к тому, что он в силах видеть, считая, что это действительно достовернее тех вещей, которые ему показывают?
— Да, это так.»© Платон «Государство», книга 7: Миф О Пещере
Пока я писал кодмоды и статью, у меня возникла идея вывести идею трейсера на более серьезный уровень: так появился проект ESTrace. Он отслеживает посещения функций, и при этом, в отличие от прекрасного инструмента похожей направленности njsTrace умеет работать с EcmaScript Модулями и на 100% покрыт тестами.
Установка стандартная:
npm i estrace
Важно понимать один момент: ESTrace построен вокруг хуков загрузки модулей, это технология экспериментальная и может изменится в будущем, я уже с ней работал когда реализовывал аналог mock-require только для импортов и все говорит о том, что скоро эта возможность стабилизируется, как это было с EcmaScript Модулями.
Как устроены лоадеры?
Принцип работы очень прост, есть несколько вариантов событий, в которые можно внедриться и повлиять на их работу, к примеру мне понадобился лоадер transformSource, который позволяет на лету менять прочитанный исходный код:
export async function transformSource(source, context) {
const {url} = context;
// добавляем события слежки в функции считанного кода
const code = await trace({
source: source.toString(),
url,
});
// возвращаем новый код
return {
source: code,
};
}
А можно использовать ESTrace как плагин для Putout?
Конечно, ESTrace экспортирует плагин, который может быть передан в putout напрямую:
import putout from 'putout';
import estracePlugin from 'estrace/plugin';
const source = `
const fn = (a) => a;
`;
const {code} = putout(source, {
plugins: [
['estrace', estracePlugin],
],
});
console.log(code);
Проверим на конкретном примере, назовем файл lint.js:
const processFile = (a) => a;
process([]);
function process(runners) {
const files = getFiles(runners);
const linted = lintFiles(files);
return linted;
}
function getFiles(runners) {
const files = [];
for (const run of runners) {
files.push(...run());
}
return files;
}
function lintFiles(files) {
const linted = [];
for (const file of files) {
linted.push(processFile(file));
}
return linted;
}
После чего запустим в консоли:
![Когда код это данные - 6 Когда код это данные - 6](https://www.pvsm.ru/images/2021/06/23/kogda-kod-eto-dannye-6.png)
Это супер круто! У меня никогда не было таких детальных логов.
❒ Источники:
![Когда код это данные - 7 Когда код это данные - 7](https://www.pvsm.ru/images/2021/06/23/kogda-kod-eto-dannye-7.jpg)
Автор: coderaiser