Что общего между Putout и Rulegurd?

от автора

Недавно наткнулся на статью про статический анализатор Ruleguard и хотел написать к ней комментарий, но получилась статья.

Интересно, что похожие идеи могут в одно время прийти разным людям, пишущим на разных языках. Я работаю над статическим анализатором Putout, и обратил внимание, что синтаксис, используемый в этих двух инструментах очень похож. В статье будет много кода на JavaScript и Go, языке который я совсем не знаю, тем не менее схожесть используемых подходов, не могла не вызвать у меня интерес.

А в конце вас ждет опрос :).

Время жизни

Рассмотрим хронологию:

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

Правила

Сравним примеры правила, которое упрощает выражение !!x до x.
Для ruleguard, код будет выглядеть таким образом:

package gorules  import "github.com/quasilyte/go-ruleguard/dsl/fluent"  func simplify(m fluent.Matcher) {     m.Match(`!!$x`)      .Suggest(`$x`)      .Report(`can simplify !!$x to $x`) }

А вот (упрощенный и адаптированный) пример правила putout remove-double-negations:

module.exports.report = () => 'can simplify !!x to x'; module.exports.replace = () => ({     '!!__x': '__x' });

Обратите внимание, что ruleguard, использует $x для обозначения переменных (интересно разворачивает ли функция Report переменную $x), а putout, для той же цели, использует двойное нижнее подчеркивание __x. Почему в putout не был выбран символ $? Одна из причин это наличие валидных, (хоть и не часто) используемых идентификаторов в популярных библиотеках, а еще похожесть на один очень практичный язык программирования, где все переменные начинаются с этого символа :), а так же выразительность двойного подчеркивания. Тут список всех поддерживаемых переменных.

Судя по всему, автор ruleguard тоже изначально использовал ‘__’, но в какой-то момент отдал предпочтение знаку доллара, который лаконичнее.

Работа с AST

Вначале правила putout, писались таким образом, что AST парсилось отдельно каждым плагином, таким образом:

// возвращаем ошибку соответствующую каждому из найденых узлов module.exports.report = () => 'Unexpected "debugger" statement';  // ищем узлы, содержащией debugger с помощью паттерна Visitor module.exports.find = (ast, {traverse}) => {     const places = [];      traverse(ast, {         DebuggerStatement(path) {             places.push(path);         }     });      return places; };  // удаляем код, найденный в предыдущем шаге module.exports.fix = (path) => {     path.remove(); };

Как верно заметил Alternator:

Каждый плагин выполняет свой traverse, что медленнее чем один комбинированный traverse, выполненный ядром.

Поэтому, обход всего AST (по возможности) выполняется единожды, поочередно заходя в нужные плагины (что позволяет 60-ти плагинам отрабатывать за вменяемое время), которые в самом простом виде могут выглядеть так:

module.exports.report = () => 'Unexpected "debugger" statement'; module.exports.replace = () => ({     'debugger': '', });

Однако в ruleguard, судя по всему, каждый раз при вызове Match выполняется обход всего дерева, в поисках нужного места кода, quasilyte, это так, или я не до конца понимаю механизм?

Ели это так, то:

  • да, это медленно
  • это не дает контекста

Дело в том, что при разработке плагинов гораздо чаще появляется необходимость детально исследовать найденные узлы, как (и сколько раз) переменные используются, где они объявлены (и объявлены ли), в каком окружении используются. К примеру в плагине convert-for-in-to-for-of, который делает следующее:

-for (const name in object) { -   if (object.hasOwnProperty(name)) { +for (const name of Object.keys(object)) {     console.log(a); -   } }

Работает таким образом:

const {     generate,     operator, } = require('putout');  const {     contains,     getTemplateValues, } = operator;  module.exports.report = () => `for-of should be used instead of for-in`;  // отфильтровуем необходимые данные // важно что бы for-in имел `hasOwnProperty` // иначе реализация for-of будет работать иначе module.exports.match = () => ({     'for (__a in __b) __body': ({__a, __b, __body}) => {         const declaration = getTemplateValues(__a, 'var __a');         const {name} = declaration.__a;          return contains(__body, [             `if (${__b.name}.hasOwnProperty(${name})) __body`,         ]);     }, });  // выполняем замену, если функция прошла фильтр module.exports.replace = () => ({     'for (__a in __b) __body': ({__b, __body}) => {         const [first] = __body.body;         const condition = getTemplateValues(first, 'if (__b.hasOwnProperty(__a)) __body');         const {code} = generate(condition.__body);          return `for (const ${condition.__a.name} of Object.keys(${__b.name})) ${code}`;     }, });

Конечно, в Go нет таких конструкций (там другие не менее полезные :)), однако необходимость в том, что бы знать больше об узлах, которые обрабатываются остается прежней, и мне пока не понятно как это можно сделать с помощью функции Match из ruleguard. На помощь могут прийти фильтры, но они в основном связаны с типами. Возможно я просто пока не понял, как эту функциональность можно расширить, либо этот функционал (на что я надеюсь) появится в новых версиях Ruleguard.

Плагины

Putout поддерживает 5 видов подгрузки плагинов:

  • @putout/plugin-name — core-плагины из папки node_modules
  • putout-plugin-nameuserland-плагины из node_modules
  • ~/.putoutcodemods, которые нужны для текущих проектов
  • с помощью флага --rulesdir, к примеру, из папки текущего проекта
  • с помощью параметра командной строки -t, --transform

По поводу ruleguard quasilyte говорит следующее:

Правила ruleguard подгружаются на старте, из специального gorules файла, декларативно описывающего паттерны кода, на которые стоит выдавать предупреждения. Этот файл может свободно редактироваться пользователями ruleguard.

И в этом есть плюс и минус:

  • плюс в том, что можно правила обработки проекта записать в специальный файл, писать туда правила и переиспользовать их для нового кода;
  • минус — в невозможности переиспользования написанного кода для других проектов (файл необходимо копировать, к файлу не понятно как писать тесты, и не понятно как выбрать нужные правила для использования в других проектах)

Командная строка

Gogrep (на котором основан Ruleguard) вдохновил меня на то, что бы добавить возможность обрабатывать данные из командной строки в Putout, таким образом:

putout --transform 'module.exports = __a -> const cheers = __a' index.js

Ищем конструкцию module.exports = __a, где __a — любая валидная конструкция JavaScript. Для автоматического исправления достаточно передать флаг fix, и тогда код:

module.exports = {     hello: 'world', };

Превратится в:

const cheers = {     hello: 'world', };

Послесловие

В целом я считаю, что это правильное направление: работа с AST самым простым из возможных путей, и популяризация идеи в массы :). На Go никогда не писал, но смотрю, что для работы с AST, есть все необходимое при чем, как я понимаю, из коробки.

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

Буду рад любым комментариям с предложениями по обработке синтаксиса на JavaScript (и Go, если quasilyte подключится к беседе).

И, на последок, опрос.

ссылка на оригинал статьи https://habr.com/ru/post/506234/


Комментарии

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

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