Анализ AST в Go с помощью инструментов JSON

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

Например, такими инструментами могут быть:

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

Решение таких задач, казалось бы, должно привести нас к сложным темам компиляторов и парсеров. Но в 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.

Оцените статью
devanswers.ru
Добавить комментарий