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

Инструментирование JavaScript путем изменения кода: области применения и общие принципы работы

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

Инструментирование JavaScript кода может понадобиться по целому ряду причин. Наиболее распространные: отладка, профилирование, трассировка, логирование. Как правило, движки в которых выполняется JavaScript предоставляют способы инструментирования кода без его изменения. В своей прошлой статье [1] я описал некоторые средства которыми это осуществляется, а тажке существующие ограничения, в конечном итоге сподвигшие меня на начало описанного в той статье проекта и изучение вопроса инструментирования JavaScript путем автоматического изменения кода. Эта тема на мой взгляд обделена вниманием, но заслуживает раскрытия, тем более в комментариях был выражен интерес к концептуальному подходу модификации кода.

Итак, зачем и как можно автоматически изменять код?

Для простейшей отладки, например, может понадобиться изменить каждую функцию скрипта обернув ее тело в try-catch блок.

Простая трассировка или логирование может осуществляться вставкой console.log, профилирование вставкой console.time/console.profile в начало и конец каждой функции, или, если точность замера не так важна или выполняющая среда не поддерживает console.time/console.profile, старым добрым Date.now().

Более глубокая и полная трассировка может понадобиться для последующего анализа тестового или сценарного покрытия кода. Собираемая инструментационными инструкциями информация о выполнении кода сохраняется туда, откуда инструмент может позже взять ее для отчета. Качественный анализ тестового покрытия предполагает отслеживание выполнения (а соответственно инструментирование) не только строк кода, но и ветвей логических и тернарных операторов.

function Foo(arg1, arg2) {
   if (arg1 || arg2 > 0)
      Bar1();
   return arg2 ? Bar2() : false;
}

=>
// Пример вымышленного, недеструктивного изменения
function Foo(arg1, arg2) {
   try {
      ping('Foo invoked');
      if ((ping('arg1 check'), arg1) || (ping('arg2 check'), arg2 > 0)) {
         ping('if branch');
         Bar1();
      }
      return arg2 ? (ping('Bar2 branch'), Bar2()) : (ping('false branch'), false);
   }
   finally {
      ping('Foo finished');
   }
}

Инструментирование кода для последующей трассировки такого рода осуществляют инструменты code coverage. Из тех с которыми мне пришлось и понраивлось работать не могу не отметить istanbul [2]. Инструмент написан на JavaScript, что в том числе помогает его популярности в использовании в grunt расширениях [3]. Я использую istanbul вместе с Jasmine [4] как для анализа покрытия тестами клиентского кода (PhantomJs [5] плюс grunt-template-jasmine-istanbul [6]), так и серверного (с grunt-jasmine-node-coverage [7]). Взглянуть на пример отчета покрытия кода istanbul для самого себя можно здесь [8].

Инструментирование JavaScript путем изменения кода: области применения и общие принципы работы

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

Каким же образом можно изменить автоматически изменять JavaScript код, находить нужные места и вставлять туда инструментационные инструкции? Можно конечно пытаться сделать это регулярными выражениями и вызвать дьявола, как в этом stackoverflow ответе [9], но правильный ответ на этот вопрос следующий: JavaScript код нужно парсить, обходить полученное абстрактное синтаксическое дерево, изменять интересующие нас узлы, преобразовывать измененное дерево назад в код.

Существует множество легко находимых парсеров JavaScript, некоторые мы используем постоянно, даже уже и не задумываясь о том, что это еще и парсер (например, uglify.js [10] или различные beautifier-ы JavaScript). В своем проекте я использовал esprima для получения изначального синтаксического дерева. Дерево представляет собой иерархический JSON, описывающий анализируемый код. Поиграть с синтаксическими деревьями, а также посмотреть другие примеры использования esprima, можно на сайте инструмента [11].

Инструментирование JavaScript путем изменения кода: области применения и общие принципы работы

Обход дерева с модификацией я реализовал без дополнительных инструментов. Тем не менее, такие инструменты существуют, например falafel [12] и burrito [13], и избавляют от написания инфраструктуры для обхода дерева и позволяют сконцентриоваться на задаче по поиску и модификации нужных узлов.

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

Генерацию кода для измененного дерева я произвожу с помощью escodegen [14], который понимает формат синтаксического дерева, выдаваемого esprima.

К сожалению, разные парсеры/генераторы вольны использовать и используют различные форматы синтаксических деревьев. К счастью, несколького популярных парсеров используют формат синтаксического дерева SpiderMonkey parser API [15], и esprima/escodegen входят в число этих парсеров/генераторов.

Для того чтобы при отладке спрятать инструментационные инструкции и заставить клиентский код в отладчике выглядеть так, как будто он не инструментирован, при генерации кода измененного дерева можно использовать source maps [16]. С использованием escodegen, все что для этого нужно, это установка одного флага (options.sourceMap).

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

В протитипе проекта я поголовно оборачивал все что можно в блоки, то есть

for (var x in y) {
      // тело цикла
}

превращалось в
{ 
   for (var x in y) {
      // тело цикла
   }
}

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

Читатель может при желании проверить свои знания/память до чтения ответа

Знать о том, что в языке есть labels я конечно знал, но не догадывался что они такие капризные. Ломающим сценарием было:

l1: for (var x in y) {
   continue l1;
}

Автор: SpyJs

Источник [17]


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

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

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

[1] В своей прошлой статье: http://habrahabr.ru/post/188708/

[2] istanbul: http://gotwarlost.github.io/istanbul/

[3] grunt расширениях: http://gruntjs.com/plugins

[4] Jasmine: http://pivotal.github.io/jasmine/

[5] PhantomJs: http://phantomjs.org/

[6] grunt-template-jasmine-istanbul: https://npmjs.org/package/grunt-template-jasmine-istanbul

[7] grunt-jasmine-node-coverage: https://npmjs.org/package/grunt-jasmine-node-coverage

[8] можно здесь: http://gotwarlost.github.io/istanbul/public/coverage/lcov-report/istanbul/lib/command/check-coverage.js.html

[9] stackoverflow ответе: http://stackoverflow.com/a/1732454/2644022

[10] uglify.js: https://github.com/mishoo/UglifyJS

[11] на сайте инструмента: http://esprima.org/demo/parse.html

[12] falafel: https://github.com/substack/node-falafel

[13] burrito: https://github.com/substack/node-burrito

[14] escodegen: https://github.com/Constellation/escodegen

[15] SpiderMonkey parser API: https://developer.mozilla.org/en-US/docs/SpiderMonkey/Parser_API

[16] source maps: http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/

[17] Источник: http://habrahabr.ru/post/188990/