Что общего между Putout и Rulegurd?
- пятница, 12 июня 2020 г. в 00:28:42
Недавно наткнулся на статью про статический анализатор 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 тоже изначально использовал '__', но в какой-то момент отдал предпочтение знаку доллара, который лаконичнее.
Вначале правила 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();
};
Каждый плагин выполняет свой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 видов подгрузки плагинов:
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 подключится к беседе).
И, на последок, опрос.