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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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


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


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


Время жизни


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


  • 30.11.2018 — первый коммит Putout
  • 12.12.2019 — первый коммит Ruleguard

Как не сложно заметить, разработка 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-name — userland-плагины из node_modules
  • ~/.putout — codemods, которые нужны для текущих проектов
  • с помощью флага --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 подключится к беседе).


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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Какой формат шаблонов для правил выразительнее:

  • 20,0%Двойное подчеркивание __ (__a, __object)1
  • 80,0%Доллар $ ($x, $_, $)4
  • 0,0%Я напишу свой вариант в комментарии0
Источник: https://habr.com/ru/post/506234/


Интересные статьи

Интересные статьи

Новый сервис WebWormHole работает как портал, через который файлы передаются с компьютера на другой. Нажимаете кнопку New Wormhole — и получаете код для входа. Человек с другой стороны вводит...
VUE.JS - это javascript фрэймворк, с версии 18.5 его добавили в ядро битрикса, поэтому можно его использовать из коробки.
В 1С-Битрикс: Управление сайтом (как и в Битрикс24) десятки, если не сотни настраиваемых типов данных (или сущностей): инфоблоки, пользователи, заказы, склады, форумы, блоги и т.д. Стр...
Disclaimer. Костис Капелонис — Developer advocate (человек, защищающий и отстаивающий принципы программной разработки) Codefresh, первой платформы CI/CD для Kubernetes и контейнеров. Миссия Codef...
Приветствую вас (лично вас, а не всех кто это читает)! Сегодня мы: Создадим приложение (навык) Алисы с использованием нового (октябрь 2019) сервиса Yandex Cloud Functions. Настроим н...