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

Alex Alex 07 липня 2020
gogrep: структурний пошук і заміна Go коду

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 на ++:

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

Приклад з реального життя: якось захотів виконати заміну slice[:] -> slice. Специфіка в тому, що не можна просто шукати [:], тому що брати такий слайс від масиву має сенс, а от від рядка або слайсу — ні.

Ось приклад того, як можна знайти зайві слайси від []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/байт/байт.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 коду

Кожна команда запитує пошуковий шаблон:

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

Результати друкуються в канал (channel output) gogrep:

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

Для search and replace потрібно розділяти частини "Find" і "Replace" токеном >:

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

Якщо прибрати з шаблону !, то замість змін файлів на місці будуть роздруковані кандидати для заміни.

Приклад пошуку тих самих комбінованих append (але без replace):

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

За замовчуванням за командами розширення не призначено жодних гарячих клавіш. Якщо вам потрібен швидший доступ до пошуку, ви можете призначити їх самостійно, дотримуючись особистих уподобань ергономіки.

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

Розширення не надає можливостей використовувати атрибути або довільні конвеєри. Інтегровані тільки x та s.

Якщо вам не вистачає якогось функціоналу або ви знайшли баг, не соромтеся і не лінуйтеся відкривати issue на GitHub.

Висновок

Сподіваюся, ця замітка допоможе цьому чудовому інструменту стати хоча б трохи популярніше.

Для автоматичного рефакторінгу, наприклад, при збереженні файлу, можна використовувати 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 синтаксис.

Додаткові матеріали:

Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.