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
. Специфіка в тому, що не можна просто шукати [:]
, тому що брати такий слайс від масиву має сенс, а от від рядка або слайсу — ні.
Ось приклад того, як можна знайти зайві слайси від []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
):
Кожна команда запитує пошуковий шаблон:
Результати друкуються в канал (channel output) gogrep
:
Для search and replace потрібно розділяти частини "Find" і "Replace" токеном >
:
Якщо прибрати з шаблону !
, то замість змін файлів на місці будуть роздруковані кандидати для заміни.
Приклад пошуку тих самих комбінованих append
(але без replace):
За замовчуванням за командами розширення не призначено жодних гарячих клавіш. Якщо вам потрібен швидший доступ до пошуку, ви можете призначити їх самостійно, дотримуючись особистих уподобань ергономіки.
Поки що автоматична установка бінарника 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
синтаксис.
Додаткові матеріали:
Ще немає коментарів