Существует множество специфических задач, которые могут значительно улучшить и автоматизировать ваше текущее сопровождение большого проекта. Некоторые из них требуют создания инструментов, которые могут анализировать или изменять исходный код, созданный разработчиками.
Например, такими инструментами могут быть:
- сбор метаданных из комментариев,
- сбор строк, которые необходимо перевести,
- понимание структуры кода для расчета метрик сложности или построения поясняющих диаграмм,
- или даже применять некоторые шаблоны автоматической оптимизации и рефакторинга кода.
Решение таких задач, казалось бы, должно привести нас к сложным темам компиляторов и парсеров. Но в 2022 году каждый современный язык программирования поставляется с батарейками в комплекте. Структура кода в виде AST, готовая к поиску и манипулированию, представлена в виде встроенной библиотеки. В принципе, разбирать файлы с кодом и искать конкретные вещи не намного сложнее, чем делать то же самое для JSON или XML.
В этой статье мы рассмотрим анализ AST в Go.
Существующие подходы
В golang существует стандартный пакет ast
, который предоставляет структуры узлов AST и функции для разбора исходных файлов. Опытным разработчикам go довольно легко и просто написать код для этого инструмента. Также существует пакет printer
, который может преобразовать AST обратно в исходный код.
Вот список статей, описывающих, как работать с AST в golang:
- Basic AST Traversal in Go
- Крутые штуки с пакетом AST в Go
- Инструментирование кода Go с помощью AST
- Переписывание исходного кода Go с помощью инструментов AST
Один небольшой аспект — вам нужно знать структуру golang AST. Для меня, когда я впервые погрузился в эту тему, проблемой было понять, как узлы объединяются вместе, и выяснить, что именно мне нужно искать в структуре узлов. Конечно, вы можете распечатать AST, используя встроенные возможности. Вы получите вывод в каком-то странном формате:
0 *ast.File {
1 . Package: 1:1
2 . Name: *ast.Ident {
3 . . NamePos: 1:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 2) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: 3:1
9 . . . Tok: import
10 . . . Lparen: 3:8
....
Кроме того, поскольку формат очень специфичен, вы не можете использовать никакие инструменты для навигации по нему, кроме текстового поиска. Инструменты типа goast-viewer могут помочь в этом, но их возможности ограничены.
Предлагаемое решение
Я начал думать о библиотеке, которая позволила бы нам конвертировать AST в какой-нибудь очень обычный формат, например JSON. JSON легко манипулировать, и существует множество инструментов (например, jq) и подходов для поиска и модификации JSON.
В итоге я остановился на asty.
Asty — это небольшая библиотека, написанная на go, которая позволяет разбирать исходный код и представлять его в структуре JSON. Но, более того, она позволяет выполнять и обратное преобразование. Это означает, что теперь вы можете манипулировать кодом go с помощью инструмента или алгоритма, разработанного на любом языке программирования.
Вы можете использовать его как пакет go, как отдельный исполняемый файл или даже как контейнер docker. Попробуйте на этой странице поэкспериментировать с asty в web-сборке.
Пример кода go:
package main
import "fmt"
func main() {
fmt.Println("hello world")
}
Пример вывода JSON:
{
"NodeType": "File",
"Name": {
"NodeType": "Ident",
"Name": "main"
},
"Decls": [
{
"NodeType": "GenDecl",
"Tok": "import",
"Specs": [
{
"NodeType": "ImportSpec",
"Name": null,
"Path": {
"NodeType": "BasicLit",
"Kind": "STRING",
"Value": ""fmt""
}
}
]
},
{
"NodeType": "FuncDecl",
"Recv": null,
"Name": {
"NodeType": "Ident",
"Name": "main"
},
"Type": {
"NodeType": "FuncType",
"TypeParams": null,
"Params": {
"NodeType": "FieldList",
"List": null
},
"Results": null
},
"Body": {
"NodeType": "BlockStmt",
"List": [
{
"NodeType": "ExprStmt",
"X": {
"NodeType": "CallExpr",
"Fun": {
"NodeType": "SelectorExpr",
"X": {
"NodeType": "Ident",
"Name": "fmt"
},
"Sel": {
"NodeType": "Ident",
"Name": "Println"
}
},
"Args": [
{
"NodeType": "BasicLit",
"Kind": "STRING",
"Value": ""hello world""
}
]
}
}
]
}
}
]
}
asty также способен выводить комментарии, позиции лексем в исходном тексте и идентификаторы ссылок. В некоторых местах AST языка go на самом деле не является деревом, а скорее DAG. Поэтому узлы могут иметь одинаковые идентификаторы, указанные в JSON.
Принципы разработки и ограничения
При разработке asty я старался следовать некоторым правилам:
- Сделать вывод JSON как можно ближе к реальным структурам golang. Не вводить никакой дополнительной логики. Никакой нормализации. Никакой реинтерпретации. Единственное, что было введено, это имена некоторых значений перечислений. Даже имена полей сохраняются в том же виде, в каком они существуют в пакете go
ast
. - Сделайте это очень явным. Никакого отражения. Никаких перечислений полей. Это сделано для облегчения будущего сопровождения. Если что-то будет изменено в будущих версиях golang, этот код, вероятно, сломается при компиляции. Буквально, asty содержит 2 копии для каждой структуры AST-узла, чтобы определить маршалинг и размаршалинг JSON.
- Сохраняйте полиморфизм в структуре JSON. Если какое-то поле ссылается на выражение, то конкретный тип будет различаться по имени типа объекта, хранящемуся в отдельном поле
NodeType
. Этого сложно добиться, поэтому если вам нужно что-то подобное для других задач, я бы рекомендовал посмотреть этот пример https://github.com/karaatanassov/go_polymorphic_json.
Другие решения
Я долго искал существующие реализации этого подхода. К сожалению, удалось найти не так много.
Один проект goblin, который я пытался использовать некоторое время, является довольно хорошим и зрелым, но в нем отсутствует поддержка обратного преобразования из JSON в AST. Он пытается переосмыслить некоторые структуры в AST, чтобы (я полагаю) упростить и сделать их более человекочитаемыми. Мое личное мнение — это не очень хорошо. Но главная проблема с ним — отсутствие поддержки. Он был разработан давным-давно для версии 1.16 и с тех пор не обновлялся. Тем не менее, вы можете найти его форк, относительно актуальный на сегодняшний день.
Другой проект go2json, генерирует JSON из кода go. Также отсутствует обратное преобразование и плохо поддерживается. И он реализован как отдельный парсер с javascript. Я думаю, что таким образом его очень трудно поддерживать и следить за новыми возможностями golang.
Дальнейшая работа
Я ищу сотрудничества с другими разработчиками, заинтересованными в разработке языковых инструментов. Тем временем, вы можете проверить другой репозиторий с примерами, где я экспериментирую с AST JSON в python.