gogrep: структурный поиск и замена Go кода

в 21:54, , рубрики: AST, extension, Go, gogrep, golang, open source, pattern matching, replace, search, search-and-replace, semantical search, ssr, structural search, vscode, Проектирование и рефакторинг, Софт

gogrep — это одна из моих любимых утилит для работы с Go. Она позволяет находить код по синтаксическим шаблонам, фильтровать результаты по типам выражений, а также выполнять замену (тоже по шаблону).

В этой заметке я расскажу как использовать gogrep, а также о VS Code расширении для более удобной работы с gogrep прямо из редактора.

gogrep: структурный поиск и замена Go кода - 1

Зачем нужен 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 на ++:

gogrep: структурный поиск и замена Go кода - 2

Пример из реальной жизни: как-то захотел выполнить замену 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):

gogrep: структурный поиск и замена Go кода - 3

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

gogrep: структурный поиск и замена Go кода - 4

Результаты печатаются в канал (output channel) gogrep:

gogrep: структурный поиск и замена Go кода - 5

Для search and replace нужно разделять части "Find" и "Replace" токеном ->:

gogrep: структурный поиск и замена Go кода - 6

Если убрать из шаблона !, то вместо изменений файлов inplace в канал будут распечатаны кандидаты для замены.

Пример поиска тех самых комбинируемых append (но без replace):

gogrep: структурный поиск и замена Go кода - 7

По умолчанию за командами расширения не назначено никаких горячих клавиш. Если вам нужен более быстрый доступ к поиску, вы можете назначить их самостоятельно, следуя личным предпочтениям эргономики.

Пока что автоматическая установка бинарника gogrep предусмотрена только для GOARCH=amd64 и GOOS=linux|windows|darwin.

Расширение не предоставляет возможностей использовать атрибуты или произвольные конвейеры. Интегрированы только -x и -s.

Если вам не хватает какого-то функционала или вы нашли баг, не стесняйтесь и не ленитесь открывать issue на GitHub.

Заключение

Надеюсь, эта заметка поможет этому замечательному инструменту стать хотя бы немного популярнее.

Если вы используете продукты JetBrains, то вам может быть знаком механизм structural search and replace (SSR). Он решают ту же задачу, но, в отличие от SSR, gogrep удобнее запускать в произвольном окружении, так как это обычная утилита командной строки.

Дополнительные материалы:

Автор: Искандер

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js