gogrep — это одна из моих любимых утилит для работы с Go. Она позволяет находить код по синтаксическим шаблонам, фильтровать результаты по типам выражений, а также выполнять замену (тоже по шаблону).
В этой заметке я расскажу как использовать gogrep
, а также о VS Code расширении для более удобной работы с gogrep
прямо из редактора.
Зачем нужен gogrep
Если в тезисах, то gogrep
может быть полезен при:
- Рефакторинге
- Изучении кодовой базы
- Поиске подозрительного кода (пример: ruleguard)
Рассмотрим пример, который демонстрирует изящность и эффективность структурного поиска.
Функции a()
и b()
выполняют одинаковые операции:
func a(xs []int) []int {
xs = append(xs, 1)
xs = append(xs, 2)
return xs
}
func b(xs []int) []int {
xs = append(xs, 1, 2)
return xs
}
Допустим, мы хотим переписать все места, где вызовы append
можно схлопнуть.
Попробуем gogrep
:
- Находим все подходящие пары с помощью
-x
шаблона$x=append($x,$a); $x=append($x,$b)
- Через
-s
шаблон$x=append($x,$a,$b)
получаем искомую замену - Передавая аргумент
-w
все затронутые файлы будут обновлены.
gogrep -w -x '$x=append($x,$a);$x=append($x,$b)' -s '$x=append($x,$a,$b)' ./...
Если поставить расширение для VS Code, то становится ещё проще.
Вот пример замены +=1
на ++
:
Пример из реальной жизни: как-то захотел выполнить замену slice[:] -> slice
. Даже заводил issue в staticcheck. Специфика в том, что нельзя просто искать [:]
, потому что брать такой слайс от массива имеет смысл, а вот от строки или слайса — нет.
Вот пример того, как можно найти лишние слайсы от []byte
в stdlib:
# Только поиск.
gogrep -x '$s[:]' -a 'type([]byte)' std
# Поиск+замена.
gogrep -x '$s[:]' -a 'type([]byte)' -s '$s' -w std
Показываю только первые 30 результатов (всего их 300+):
$GOROOT/src/archive/tar/format.go:163:59: b[:]
$GOROOT/src/archive/tar/reader.go:345:33: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:348:17: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:348:28: zeroBlock[:]
$GOROOT/src/archive/tar/reader.go:349:34: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:352:18: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:352:29: zeroBlock[:]
$GOROOT/src/archive/tar/reader.go:396:23: tr.blk[:]
$GOROOT/src/archive/tar/reader.go:497:36: blk[:]
$GOROOT/src/archive/tar/reader.go:528:33: blk[:]
$GOROOT/src/archive/tar/reader.go:531:14: blk[:]
$GOROOT/src/archive/tar/writer.go:392:26: blk[:]
$GOROOT/src/archive/tar/writer.go:477:23: zeroBlock[:]
$GOROOT/src/archive/zip/reader.go:233:29: buf[:]
$GOROOT/src/archive/zip/reader.go:236:15: buf[:]
$GOROOT/src/archive/zip/reader.go:251:30: buf[:]
$GOROOT/src/archive/zip/reader.go:254:15: buf[:]
$GOROOT/src/archive/zip/writer.go:92:17: buf[:]
$GOROOT/src/archive/zip/writer.go:110:19: buf[:]
$GOROOT/src/archive/zip/writer.go:116:30: buf[:]
$GOROOT/src/archive/zip/writer.go:132:27: buf[:]
$GOROOT/src/archive/zip/writer.go:157:17: buf[:]
$GOROOT/src/archive/zip/writer.go:177:27: buf[:]
$GOROOT/src/archive/zip/writer.go:190:16: buf[:]
$GOROOT/src/archive/zip/writer.go:198:26: buf[:]
$GOROOT/src/archive/zip/writer.go:314:18: mbuf[:]
$GOROOT/src/archive/zip/writer.go:319:31: mbuf[:]
$GOROOT/src/archive/zip/writer.go:386:16: buf[:]
$GOROOT/src/archive/zip/writer.go:398:23: buf[:]
$GOROOT/src/bytes/bytes.go:172:24: b[:]
Поисковые шаблоны
Поисковой шаблон — это небольшой фрагмент Go кода, который может включать в себя $-выражения (мы будем называть их "переменными шаблона"). Шаблон может быть выражением, statement (или их списком) или декларацией.
Переменные шаблона — это Go переменные с префиксом $
. Переменные шаблона с одинаковым именем всегда захватывают идентичные элементы AST. Исключением является переменная с именем $_
, их можно использовать для обозначения "что угодно".
Перед именем переменной шаблона можно поставить *
, тогда переменная будет захватывать произвольное количество элементов.
Поисковой шаблон | Интерпретация |
---|---|
$_ |
Что угодно. |
$x |
Идентично первому примеру, "что угодно". |
$x = $x |
Самоприсваивание. |
(($_)) |
Любое выражение в двойных скобках. |
if $init; $cond {$x} else {$x} |
if с дублирующимися then/else блоками. |
fmt.Fprintf(os.Stdout, $*_) |
Вызов Fprintf с аргументом os.Stdout . |
Как уже демонстрировалось в примере с append()
, шаблон может содержать несколько statement'ов. Нотация "$x; $y
" означает "найди $x, за которым следует $y".
gogrep
выполняет честный backtracking для шаблонов с *
. К примеру, шаблоном можно найти все map
литералы, где есть хотя бы один дублирующийся ключ:
map[$_]$_{$*_, $key: $val1, $*_, $key: $val2, $*_}
Конвейеры и команды gogrep
Ранее мы использовали параметры -x
и -s
, не разбирая что они из себя представляют.
gogrep
оперирует командами, которые составляют конвейер (pipeline). Порядок команд имеет значение. Полный синопсис выглядит следующим образом:
gogrep commands... [targets...]
target
может быть файлом, директорией или пакетом. Всё эквивалентно тому, как обрабатывает аргументы команда go build
.
Команда | Описание |
---|---|
-x pattern |
Найти все элементы AST, которые подходят под pattern . |
-g pattern |
Отбросить результаты, которые не подходят под pattern . |
-v pattern |
Отбросить результаты, которые подходят под pattern . |
-a attr |
Отбросить результаты, которые не имеют атрибута attr . |
-s pattern |
Переписать результат, используя pattern . |
-p n |
Для каждого результата, подняться на n уровней по AST. |
Как можно догадаться, -x
чаще всего является первой командой в конвейере. Затем могут следовать фильтрующие команды или модифицирующие команды.
Рассмотрим это всё на примерах.
// file foo.go
package foo
func bar() {
println(1)
println(2)
println(3)
}
# Находим все вызовы println()
$ gogrep -x 'println($*_)' foo.go
foo.go:4:2: println(1)
foo.go:5:2: println(2)
foo.go:6:2: println(3)
# Добавляем команды -v для отбрасывания всех результатов,
# где есть литерал 1, а затем литерал 2.
$ gogrep -x 'println($*_)' -v 1 -v 2 foo.go
foo.go:6:2: println(3)
# Дополнительно поднимаемся на 2 уровня выше
# и доходим до содержащего *ast.BlockStmt.
$ gogrep -x 'println($*_)' -v 1 -v 2 -p 2 foo.go
foo.go:3:12: { println(1); println(2); println(3); }
Атрибутов довольно много, большая часть из них очень ситуативная, а документации на них нет совсем. Остаётся смотреть в исходниках.
Одним из наиболее полезных атрибутов является type
:
# Матчит и сложение, и конкатенацию.
gogrep -x '$lhs + $rhs'
# Матчит только конкатенацию.
gogrep -x '$lhs + $rhs' -a 'type(string)'
По умолчанию gogrep
не выполняет поиск в тестовых файлах. Чтобы это исправить, стоит передавать аргумент -tests
.
Обзор возможностей VS Code расширения
Все предоставляемые функции сводятся к нескольким командам (Ctrl+Shift+P
или Cmd+Shift+P
):
Каждая команда запрашивает поисковой шаблон:
Результаты печатаются в канал (output channel) gogrep
:
Для search and replace нужно разделять части "Find" и "Replace" токеном ->
:
Если убрать из шаблона !
, то вместо изменений файлов inplace в канал будут распечатаны кандидаты для замены.
Пример поиска тех самых комбинируемых append
(но без replace):
По умолчанию за командами расширения не назначено никаких горячих клавиш. Если вам нужен более быстрый доступ к поиску, вы можете назначить их самостоятельно, следуя личным предпочтениям эргономики.
Пока что автоматическая установка бинарника gogrep
предусмотрена только для GOARCH=amd64
и GOOS=linux|windows|darwin
.
Расширение не предоставляет возможностей использовать атрибуты или произвольные конвейеры. Интегрированы только -x
и -s
.
Если вам не хватает какого-то функционала или вы нашли баг, не стесняйтесь и не ленитесь открывать issue на GitHub.
Заключение
Надеюсь, эта заметка поможет этому замечательному инструменту стать хотя бы немного популярнее.
Если вы используете продукты JetBrains, то вам может быть знаком механизм structural search and replace (SSR). Он решают ту же задачу, но, в отличие от SSR, gogrep
удобнее запускать в произвольном окружении, так как это обычная утилита командной строки.
Для автоматического рефакторинга, например, при сохранении файла, можно использовать ruleguard с опцией -fix
:
m.Match(`fmt.Fprint(os.Stdout, $*args)`).Suggest(`fmt.Print($args)`)
m.Match(`fmt.Fprintln(os.Stdout, $*args)`).Suggest(`fmt.Println($args)`)
m.Match(`fmt.Fprintf(os.Stdout, $*args)`).Suggest(`fmt.Printf($args)`)
Эти три правила будут находить вызовы Fprint*
с аргументов Stdout
и заменять их на Print*
эквиваленты. Шаблоны в Match()
используют gogrep
синтаксис.
Дополнительные материалы:
- Daniel Marti рассказывает о gogrep
- Множество примеров gogrep шаблонов
- Аналогичный инструмент для PHP — phpgrep
- VS Code расширение для phpgrep
- golang.org/x/tools/cmd/eg
powerman
А что вообще сейчас есть из утилит для сложного рефакторинга?
Чем, например, отличаются возможности golang.org/x/tools/cmd/eg и
gogrep -s
?quasilyte Автор
Я на
eg
обращал ранее внимание, но к нему маловато документации и примеров. Выглядит как более простая утилита, чемgogrep
, я так понимаю, там нет фильтров по атрибутам и не построить конвейеры. Использовать отдельный файл с before/after выглядит не так удобно для интерактивной команды (хотя расширение могло бы создавать такой файл самостоятельно).Есть ещё малоизвестный (?)
gofmt -r
, сравнение с которым было в докладе автора gogrep.Я не знаю, сделано для Go или нет, но в IDE известной компании есть SSR, который работает для нескольких ЯП.
В ruleguard есть возможность описывать quickfix на основе тех же
gogrep
шаблонов. Разница в том, что можно хранить правила в отдельном файле, что позволит на сохранении заменять всё, что хочется упрощать автоматически. Вот простой пример:Находим вызовы
Fprintf
с аргументовStdout
и заменяем наPrint*
функции.