Когда код это данные

от автора

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

© Платон «Государство», книга 7: Миф О Пещере

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

Забегая вперед скажу, что результатом общения стал loader ESTrace, который при запуске может показать что-то вроде:

Но об этом позже, а сейчас:

▍Следим за функциями

«Люди обращены спиной к свету, исходящему от огня, который горит далеко в вышине, а между огнём и узниками проходит верхняя дорога, ограждённая невысокой стеной вроде той ширмы, за которой фокусники помещают своих помощников, когда поверх ширмы показывают кукол.»

© Платон «Государство», книга 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>'; }

которая отрабатывает так (картинка кликабельная):

▍Вносим неразбериху улучшения

«За этой стеной другие люди несут различную утварь, держа её так, что она видна поверх стены; проносят они и статуи, и всяческие изображения живых существ, сделанные из камня и дерева. При этом, как водится, одни из несущих разговаривают, другие молчат.»

© Платон «Государство», книга 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,     }); }

выглядит так (картинка кликабельная):

▍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: '❌'',     }); }

выглядит так (картинка кликабельная):

▍Выдох

«Когда с кого-нибудь из них снимут оковы, заставят его вдруг встать, повернуть шею, пройтись, взглянуть вверх — в сторону света, ему будет мучительно выполнять всё это, он не в силах будет смотреть при ярком сиянии на те вещи, тень от которых он видел раньше. И как ты думаешь, что он скажет, когда ему начнут говорить, что раньше он видел пустяки, а теперь, приблизившись к бытию и обратившись к более подлинному, он мог бы обрести правильный взгляд? Да ещё если станут указывать на ту или иную проходящую перед ним вещь и заставят отвечать на вопрос, что это такое? Не считаешь ли ты, что это крайне его затруднит и он подумает, будто гораздо больше правды в том, что он видел раньше, чем в том, что ему показывают теперь?
— Конечно, он так подумает.
— А если заставить его смотреть прямо на самый свет, разве не заболят у него глаза, и не отвернётся он поспешно к тому, что он в силах видеть, считая, что это действительно достовернее тех вещей, которые ему показывают?
— Да, это так.»

© Платон «Государство», книга 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; }

После чего запустим в консоли:

Это супер круто! У меня никогда не было таких детальных логов.

❒ Источники:


ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/563568/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *