gocorpus: открытый корпус Go кода, поддерживающий запросы
На днях я запустил wasm-приложение, которое позволяет запускать gogrep шаблоны на относительно крупном корпусе Go кода (~11 миллионов строк кода).
В этой заметке я напишу как этим пользоваться и зачем оно вообще может быть нужно.
Звёздочки нести сюда Исходный код можно найти здесь: github.com/quasilyte/gocorpus.
Зачем?
Допустим, вы хотите проверить утверждение, что в среднестатистическом Go кода так никто не пишет (или наоборот, что все так пишут). Для этого вам придётся выполнить эти шаги:
- Собрать коллекцию Go кода. Несколько репозиториев, желательно разнообразных.
- Придумать, как исполнять поиск. Регулярные выражения могут быть не самым подходящим инструментом.
- Сделать результаты воспроизводимыми для других людей, чтобы им не пришлось верить вам на слово.
Проект gocorpus решает все эти шаги за вас.
Разве что на момент написания статьи пункт (3)
не решён полностью. Другой человек может зайти на страницу и повторить запрос, но «share» не реализован. По задумке, share будет выдавать URL с опциями для запуска: шаблон поиска, фильтры, выбранные репозитории.
Поскольку gocorpus лично мне нужен был для проверки статистики, а не именно для поиска по проектам, результаты сейчас выдаются без информации о локации в исходном коде. Это не принципиальное ограничение: я планирую добавить настройку формата результатов в будущем. Но нужно учитывать, что мне интереснее добавлять агрегацию и инсайты по результатам, а не превращать это в песочницу для gogrep. Например, мне очень хотелось бы вычислять некоторый hit rate для шаблона, чтобы легче было понять частоту результата.
О существующих решениях
Я несколько раз слышал о bigquery корпусе, но, насколько мне известно, это не совсем бесплатная штука. К тому же, я считаю, что этим вариантом не очень удобно пользоваться.
codesearch работает на регулярных выражениях. Этого не всегда достаточно, как я уже упоминал выше. Такая же ситуация с grep.app (насколько я понял, там даже нельзя ограничить поиск .go
файлами).
Был ещё старенький корпус от Russ Cox, но он уже в архиве и его содержимое никогда не актуализировалось. Тем более это просто коллекция кода, а не готовое решение всё-в-одном.
Daniel Marti (оригинальный автор gogrep) собрал что-то вроде индекса популярного кода: github.com/mvdan/corpus. В теории, этот индекс можно использовать для формирования набора репозиториев, доступных в моём приложении.
Корпус кода
Для выбора репозиториев я делал поиск по GitHub с фильтрами и сортировкой по количеству звёздочек. Не самый научный подход, но он достаточно хорош для первой итерации.
После этого выбранные репозитории клонируются, анализируются, минифицируются и кладутся в tar архивы со сжатием.
По мере анализа файлов, мы записываем в метаданные некоторые метрики и факты о файле. Например, импортирует ли файл «unsafe» или «C» (cgo). Эту информацию затем можно использовать в фильтрах.
На клиенте мы качаем tar.gz
файлы и на месте их разжимаем.
Подробнее о том, как собирается корпус, можно посмотреть в makecorpus.
Фильтры
На текущий момент реализованы следующие фильтры:
$var.IsConst()
выражение, захваченное$var
является константой$var.IsPure()
выражение, захваченное$var
не имеет побочных эффектовfile.IsTest()
true для файлов с суффиксом_test.go
в названии или_test
в имени пакетаfile.IsMain()
true для файлов с именем пакетаmain
file.IsAutogen()
true для файлов, которые размечены как автоматически сгенерированные
Фильтры — это обычные Go выражения. Их можно сочетать через &&
. Также можно использовать (
и )
для группирования, а !
для инвертирования эффекта.
file
— это предопределённая переменная, которая привязана к текущему обрабатываемому файлу. $
— это переменная из шаблона поиска.
$x.IsConst() && !file.IsTest()
— не искать в тестах,$x
должен быть константным.$x.IsPure() && !$y.IsPure()
—$x
должен быть чистым выражением, а$y
— нет.!file.IsAutogen() && !file.IsTest()
— не искать в тестах и автоматически сгенерированных файлах.
Если вы пользовались ruleguard, то вам это может напомнить фильтры в Where()
.
Примеры запросов
Приведу несколько примеров поисковых шаблонов.
Одинаковые имена переменных будут требовать идентичных матчей. То есть $x = $x
находит только самоприсваивания, а $x = $y
может найти любые присваивания (в том числе и самоприсваивания). Исключением из правил является $_
— пустая переменная не требует соответствий даже если она используется несколько раз.
Вот паттерн посложнее: map[$_]$_{$*_, $k: $_, $*_, $k: $_, $*_}
. Он находит map-литералы, которые содержат ключи-дубликаты. Модификатор *
работает прямо как в регулярных выражениях: будет 0 или более матчей. Чтобы найти любые вызовы fmt.Printf
, мы можем сделать такой шаблон: fmt.Printf($*_)
.
Переменная с модификатором необязательно должна быть пустой. Например, вот это тоже валидный шаблон: fmt.Printf($format, $*args)
.
Модификатора +
нет, но его часто можно эмулировать вот так: f($_, $*_)
— вызовы f
с одним или более аргументами.
Больше примеров шаблонов можно подсмотреть в правилах go-critic.
Если вы считаете, что нужно добавить какой-то репозиторий в корпус, сообщите мне об этом. Аналогично, если не хватает какой-то фичи или вы нашли баг, тоже открываем issue и я постараюсь это исправить.
Буду рад обратной связи.
P.S. — у меня мало опыта работы с фронтендом, поэтому код на TypeScript и вёртска оставляют желать лучшего. Если кто-то поможет с этой частью, я буду очень признателен.