- PVSM.RU - https://www.pvsm.ru -

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

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

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

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

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

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

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

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

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

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

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

Я хочу получать информацию о выполнении функций, самый простой вариант console.log('function name', arguments) мне подойдёт. Если получится добавить поддержку методов будет великолепно.

Узлы содержащие функции в Babel AST [5] могут быть такими:

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, он объединяет в себе все перечисленные выше варианты.

Будем использовать Включитель [6] и экспортировать функции:

  • include, чтобы знать, что искать;
  • fix, для изменения кода;

Таким образом, функция поиска:

module.exports.include = () => [
    'Function',
];

Создавать узлы будем с помощью @babel/template [7], после чего добавим результат в начало функции:

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

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

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

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

Строить лог будем таким образом, чтобы аргументы выводились как массив, а не объект [8]:

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

▍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

▍Выдох

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

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

Пока я писал кодмоды и статью, у меня возникла идея вывести идею трейсера на более серьезный уровень: так появился проект ESTrace [4]. Он отслеживает посещения функций, и при этом, в отличие от прекрасного инструмента похожей направленности njsTrace [9] умеет работать с EcmaScript Модулями [10] и на 100% покрыт тестами.

Установка стандартная:

npm i estrace

Важно понимать один момент: ESTrace построен вокруг хуков загрузки модулей [3], это технология экспериментальная и может изменится в будущем, я уже с ней работал когда реализовывал аналог mock-require [11] только для импортов [12] и все говорит о том, что скоро эта возможность стабилизируется, как это было с EcmaScript Модулями.

Как устроены лоадеры?

Принцип работы очень прост, есть несколько вариантов событий, в которые можно внедриться и повлиять на их работу, к примеру мне понадобился лоадер transformSource [13], который позволяет на лету менять прочитанный исходный код:

export async function transformSource(source, context) {
    const {url} = context;
    
    // добавляем события слежки в функции считанного кода
    const code = await trace({
        source: source.toString(),
        url,
    });
    
    // возвращаем новый код
    return {
        source: code,
    };
}

А можно использовать ESTrace как плагин для Putout?

Конечно, ESTrace экспортирует плагин, который может быть передан в putout [14] напрямую:

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);

Проверим на конкретном примере [15], назовем файл 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

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

❒ Источники:

Автор: coderaiser

Источник [18]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/news/365463

Ссылки в тексте:

[1] диалогами Платона: https://ru.wikipedia.org/wiki/%D0%9C%D0%B8%D1%84_%D0%BE_%D0%BF%D0%B5%D1%89%D0%B5%D1%80%D0%B5

[2] линтера нового поколения: https://habr.com/ru/post/504594/

[3] loader: https://nodejs.org/api/esm.html#esm_loaders

[4] ESTrace: https://github.com/coderaiser/estrace

[5] Babel AST: https://github.com/babel/babel/blob/main/packages/babel-parser/ast/spec.md

[6] Включитель: https://github.com/coderaiser/putout/tree/master/packages/engine-runner#includer

[7] @babel/template: https://babeljs.io/docs/en/babel-template

[8] аргументы выводились как массив, а не объект: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments

[9] njsTrace: https://github.com/ValYouW/njsTrace

[10] EcmaScript Модулями: https://nodejs.org/api/esm.html

[11] mock-require: https://www.npmjs.com/package/mock-require

[12] для импортов: https://www.npmjs.com/package/mock-import

[13] transformSource: https://nodejs.org/api/esm.html#esm_transformsource_source_context_defaulttransformsource

[14] putout: https://github.com/coderaiser/putout

[15] конкретном примере: https://habr.com/ru/company/ruvds/blog/562226/

[16] ишью: https://github.com/coderaiser/putout/issues/61

[17] кот: https://www.redbubble.com/i/sticker/pixel-kitteh-by-skramzgirl/56511620.EJUG5

[18] Источник: https://habr.com/ru/post/563568/?utm_source=habrahabr&utm_medium=rss&utm_campaign=563568